Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c1f450c4b | ||
|
|
f417ec23c2 | ||
|
|
d961b44c30 | ||
|
|
2b0f6cedaa | ||
|
|
bbc34935c6 | ||
|
|
fcf6483127 | ||
|
|
6c24d290f4 | ||
|
|
396207d6b0 | ||
|
|
7a35de80d0 | ||
|
|
9f0d41d827 | ||
|
|
562448f576 | ||
|
|
cb911a3c04 | ||
|
|
06ee9a5016 | ||
|
|
0113b0112b | ||
|
|
ae8d4bd432 | ||
|
|
c9fbf64472 | ||
|
|
d93b82b1eb | ||
|
|
02c55f765f |
55
.github/workflows/tests.yml
vendored
Normal file
55
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
tools: composer:v2
|
||||
coverage: xdebug
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Copy Environment File
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate Application Key
|
||||
run: php artisan key:generate
|
||||
|
||||
- name: Create Database
|
||||
run: touch database/database.sqlite
|
||||
|
||||
- name: Rector Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/rector
|
||||
key: ${{ runner.os }}-rector-${{ hashFiles('composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-rector-
|
||||
- run: mkdir -p /tmp/rector
|
||||
|
||||
- name: PHPStan Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/phpstan
|
||||
key: ${{ runner.os }}-phpstan-${{ hashFiles('composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-phpstan-
|
||||
- run: mkdir -p /tmp/phpstan
|
||||
|
||||
- name: Tests
|
||||
run: composer test
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -2,6 +2,25 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [2.1.0](https://github.com/Grazulex/laravel-api-kit/releases/tag/v2.1.0) (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
- integrate PHPStan, Rector and Pint code quality tools (#13) ([f417ec2](https://github.com/Grazulex/laravel-api-kit/commit/f417ec23c2eada4eaddf3ec9e4cff7ccc71a3572))
|
||||
## [2.0.2](https://github.com/Grazulex/laravel-api-kit/releases/tag/v2.0.2) (2026-01-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- clean up routes after merge conflict ([fcf6483](https://github.com/Grazulex/laravel-api-kit/commit/fcf64831272a1aaf20fde0d8001714994ccc6932))
|
||||
|
||||
### Chores
|
||||
|
||||
- update dependencies and fix apiroute config (#10) ([2b0f6ce](https://github.com/Grazulex/laravel-api-kit/commit/2b0f6cedaa2e7804d0a2a30c0d8c8d0dc27c2f57))
|
||||
## [1.1.0](https://github.com/Grazulex/laravel-api-kit/releases/tag/v1.1.0) (2025-12-25)
|
||||
|
||||
### Chores
|
||||
|
||||
- **deps:** update laravel-apiroute to ^1.0 ([02c55f7](https://github.com/Grazulex/laravel-api-kit/commit/02c55f765f103852258398d7d0d0790146a33f10))
|
||||
## [1.0.0](https://github.com/Grazulex/laravel-api-kit/releases/tag/v1.0.0) (2025-12-25)
|
||||
|
||||
### Features
|
||||
|
||||
115
README.md
115
README.md
@@ -15,6 +15,7 @@ A production-ready, API-only Laravel 12 starter kit following the 2024-2025 REST
|
||||
- **Data Objects** - Type-safe DTOs via [spatie/laravel-data](https://github.com/spatie/laravel-data)
|
||||
- **Auto Documentation** - Zero-annotation OpenAPI 3.1 via [dedoc/scramble](https://github.com/dedoc/scramble)
|
||||
- **Modern Testing** - Pest PHP with Laravel HTTP testing
|
||||
- **Code Quality** - PHPStan (max level), Rector, and Pint with strict rules
|
||||
- **Rate Limiting** - Configurable per-route rate limiters
|
||||
- **Standardized Responses** - Consistent JSON response format
|
||||
|
||||
@@ -230,7 +231,9 @@ laravel-api-kit/
|
||||
│ ├── sanctum.php # Token auth config
|
||||
│ └── scramble.php # API docs config
|
||||
├── routes/
|
||||
│ └── api.php # API routes with versioning
|
||||
│ ├── api.php # API routes entry point
|
||||
│ └── api/
|
||||
│ └── v1.php # Version 1 routes
|
||||
├── tests/
|
||||
│ └── Feature/Api/V1/
|
||||
│ └── AuthTest.php # Authentication tests
|
||||
@@ -241,7 +244,7 @@ laravel-api-kit/
|
||||
|
||||
## API Versioning
|
||||
|
||||
This kit uses [grazulex/laravel-apiroute](https://github.com/Grazulex/laravel-apiroute) for API versioning with support for:
|
||||
This kit uses [grazulex/laravel-apiroute](https://github.com/Grazulex/laravel-apiroute) v2.x for API versioning with support for:
|
||||
|
||||
- **URI Path** (default): `/api/v1/users`, `/api/v2/users`
|
||||
- **Header**: `X-API-Version: 2`
|
||||
@@ -252,22 +255,34 @@ This kit uses [grazulex/laravel-apiroute](https://github.com/Grazulex/laravel-ap
|
||||
|
||||
1. Create controllers in `app/Http/Controllers/Api/V2/`
|
||||
2. Create requests in `app/Http/Requests/Api/V2/`
|
||||
3. Update `routes/api.php`:
|
||||
3. Create route file `routes/api/v2.php`:
|
||||
|
||||
```php
|
||||
use Grazulex\ApiRoute\Facades\ApiRoute;
|
||||
<?php
|
||||
|
||||
// Version 2 - New current version
|
||||
ApiRoute::version('v2', function () {
|
||||
Route::post('register', [V2\AuthController::class, 'register']);
|
||||
// ... more routes
|
||||
})->current();
|
||||
use App\Http\Controllers\Api\V2\AuthController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Version 1 - Mark as deprecated
|
||||
ApiRoute::version('v1', function () {
|
||||
Route::post('register', [V1\AuthController::class, 'register']);
|
||||
// ... existing routes
|
||||
})->deprecated('2025-06-01')->sunset('2025-12-01');
|
||||
Route::post('register', [AuthController::class, 'register']);
|
||||
// ... more routes
|
||||
```
|
||||
|
||||
4. Update `config/apiroute.php`:
|
||||
|
||||
```php
|
||||
'versions' => [
|
||||
'v1' => [
|
||||
'routes' => base_path('routes/api/v1.php'),
|
||||
'status' => 'deprecated',
|
||||
'deprecated_at' => '2025-06-01',
|
||||
'sunset_at' => '2025-12-01',
|
||||
'successor' => 'v2',
|
||||
],
|
||||
'v2' => [
|
||||
'routes' => base_path('routes/api/v2.php'),
|
||||
'status' => 'active',
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
### Deprecation Headers
|
||||
@@ -275,8 +290,8 @@ ApiRoute::version('v1', function () {
|
||||
When accessing deprecated versions, responses include RFC-compliant headers:
|
||||
|
||||
```http
|
||||
Deprecation: @1717200000
|
||||
Sunset: Sun, 01 Dec 2025 00:00:00 GMT
|
||||
Deprecation: Sun, 01 Jun 2025 00:00:00 GMT
|
||||
Sunset: Mon, 01 Dec 2025 00:00:00 GMT
|
||||
Link: </api/v2>; rel="successor-version"
|
||||
```
|
||||
|
||||
@@ -421,15 +436,60 @@ it('requires authentication', function () {
|
||||
});
|
||||
```
|
||||
|
||||
## Code Quality
|
||||
|
||||
This kit includes strict code quality tools configured following [nunomaduro/laravel-starter-kit](https://github.com/nunomaduro/laravel-starter-kit) standards.
|
||||
|
||||
### Tools
|
||||
|
||||
| Tool | Purpose | Config |
|
||||
|------|---------|--------|
|
||||
| [PHPStan](https://phpstan.org/) + [Larastan](https://github.com/larastan/larastan) | Static analysis (level max) | `phpstan.neon` |
|
||||
| [Rector](https://getrector.com/) | Automated refactoring | `rector.php` |
|
||||
| [Pint](https://laravel.com/docs/pint) | Code style (strict rules) | `pint.json` |
|
||||
|
||||
### Composer Scripts
|
||||
|
||||
```bash
|
||||
# Apply all fixes (Rector + Pint)
|
||||
composer lint
|
||||
|
||||
# Check without fixing (CI mode)
|
||||
composer test:lint
|
||||
|
||||
# Static analysis only
|
||||
composer test:types
|
||||
|
||||
# Unit tests only
|
||||
composer test:unit
|
||||
|
||||
# Full test suite (lint + types + unit)
|
||||
composer test
|
||||
```
|
||||
|
||||
### With Docker
|
||||
|
||||
```bash
|
||||
docker compose exec app composer lint
|
||||
docker compose exec app composer test
|
||||
```
|
||||
|
||||
### Strict Rules Applied
|
||||
|
||||
- `declare(strict_types=1)` on all files
|
||||
- `final` classes by default
|
||||
- Type declarations enforced
|
||||
- Dead code removal
|
||||
- Early returns
|
||||
- Strict comparisons
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
Tests run automatically on push/PR to `main` via `.github/workflows/tests.yml`.
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Code formatting (Laravel Pint)
|
||||
docker compose run --rm app ./vendor/bin/pint
|
||||
|
||||
# Check code style without fixing
|
||||
docker compose run --rm app ./vendor/bin/pint --test
|
||||
|
||||
# List all routes
|
||||
docker compose run --rm app php artisan route:list
|
||||
|
||||
@@ -574,14 +634,11 @@ class PostResource extends JsonResource
|
||||
|
||||
4. **Add Routes:**
|
||||
```php
|
||||
// routes/api.php
|
||||
ApiRoute::version('v1', function () {
|
||||
// routes/api/v1.php
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
// ... existing routes
|
||||
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::apiResource('posts', PostController::class);
|
||||
});
|
||||
})->current();
|
||||
Route::apiResource('posts', PostController::class);
|
||||
});
|
||||
```
|
||||
|
||||
5. **Create Tests:**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Api\ApiController;
|
||||
@@ -11,11 +13,11 @@ use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AuthController extends ApiController
|
||||
final class AuthController extends ApiController
|
||||
{
|
||||
public function register(RegisterRequest $request): JsonResponse
|
||||
{
|
||||
$user = User::create([
|
||||
$user = User::query()->create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
@@ -31,7 +33,7 @@ class AuthController extends ApiController
|
||||
|
||||
public function login(LoginRequest $request): JsonResponse
|
||||
{
|
||||
$user = User::where('email', $request->email)->first();
|
||||
$user = User::query()->where('email', $request->email)->first();
|
||||
|
||||
if (! $user || ! Hash::check($request->password, $user->password)) {
|
||||
return $this->unauthorized('Invalid credentials');
|
||||
@@ -47,7 +49,9 @@ class AuthController extends ApiController
|
||||
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->currentAccessToken()->delete();
|
||||
/** @var User $user */
|
||||
$user = $request->user();
|
||||
$user->currentAccessToken()->delete();
|
||||
|
||||
return $this->success(message: 'Logged out successfully');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
/**
|
||||
* @property string $email
|
||||
* @property string $password
|
||||
*/
|
||||
final class LoginRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
@@ -12,7 +19,7 @@ class LoginRequest extends FormRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RegisterRequest extends FormRequest
|
||||
/**
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property string $password
|
||||
*/
|
||||
final class RegisterRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
@@ -12,7 +20,7 @@ class RegisterRequest extends FormRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin \App\Models\User
|
||||
* @mixin User
|
||||
*/
|
||||
class UserResource extends JsonResource
|
||||
final class UserResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property Carbon|null $email_verified_at
|
||||
* @property string $password
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
*/
|
||||
final class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
use HasApiTokens;
|
||||
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
@@ -7,7 +9,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
final class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
@@ -28,23 +30,17 @@ class AppServiceProvider extends ServiceProvider
|
||||
/**
|
||||
* Configure the rate limiters for the application.
|
||||
*/
|
||||
protected function configureRateLimiting(): void
|
||||
private function configureRateLimiting(): void
|
||||
{
|
||||
// Default API rate limiter - 60 requests per minute
|
||||
RateLimiter::for('api', function (Request $request) {
|
||||
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
|
||||
});
|
||||
RateLimiter::for('api', fn (Request $request) => Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()));
|
||||
|
||||
// Auth endpoints - more restrictive (prevent brute force)
|
||||
RateLimiter::for('auth', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->ip());
|
||||
});
|
||||
RateLimiter::for('auth', fn (Request $request) => Limit::perMinute(5)->by($request->ip()));
|
||||
|
||||
// Authenticated user requests - higher limit
|
||||
RateLimiter::for('authenticated', function (Request $request) {
|
||||
return $request->user()
|
||||
? Limit::perMinute(120)->by($request->user()->id)
|
||||
: Limit::perMinute(60)->by($request->ip());
|
||||
});
|
||||
RateLimiter::for('authenticated', fn (Request $request) => $request->user()
|
||||
? Limit::perMinute(120)->by($request->user()->id)
|
||||
: Limit::perMinute(60)->by($request->ip()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -31,6 +33,9 @@ trait ApiResponse
|
||||
return response()->json(null, Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $errors
|
||||
*/
|
||||
protected function error(
|
||||
string $message = 'Error',
|
||||
int $code = Response::HTTP_BAD_REQUEST,
|
||||
@@ -41,7 +46,7 @@ trait ApiResponse
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
if (! empty($errors)) {
|
||||
if ($errors !== []) {
|
||||
$response['errors'] = $errors;
|
||||
}
|
||||
|
||||
@@ -63,6 +68,9 @@ trait ApiResponse
|
||||
return $this->error($message, Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $errors
|
||||
*/
|
||||
protected function validationError(array $errors, string $message = 'Validation failed'): JsonResponse
|
||||
{
|
||||
return $this->error($message, Response::HTTP_UNPROCESSABLE_ENTITY, $errors);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"keywords": ["laravel", "api", "rest", "starter-kit", "sanctum"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"php": "^8.3",
|
||||
"dedoc/scramble": "^0.12",
|
||||
"grazulex/laravel-apiroute": "^0.0",
|
||||
"grazulex/laravel-apiroute": "^2.0",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
@@ -16,13 +16,16 @@
|
||||
"spatie/laravel-query-builder": "^6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"driftingly/rector-laravel": "^2.0",
|
||||
"fakerphp/faker": "^1.23",
|
||||
"larastan/larastan": "^3.7",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"pestphp/pest": "^3.0",
|
||||
"pestphp/pest-plugin-laravel": "^3.0"
|
||||
"pestphp/pest": "^4.0",
|
||||
"pestphp/pest-plugin-laravel": "^4.0",
|
||||
"rector/rector": "^2.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -37,10 +40,26 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": [
|
||||
"lint": [
|
||||
"rector",
|
||||
"pint"
|
||||
],
|
||||
"test:lint": [
|
||||
"pint --test",
|
||||
"rector --dry-run"
|
||||
],
|
||||
"test:types": [
|
||||
"phpstan"
|
||||
],
|
||||
"test:unit": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"test": [
|
||||
"@test:lint",
|
||||
"@test:types",
|
||||
"@test:unit"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
|
||||
1304
composer.lock
generated
1304
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,28 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Versions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define your API versions here. Each version has its own route file,
|
||||
| middleware, and lifecycle status.
|
||||
|
|
||||
*/
|
||||
'versions' => [
|
||||
'v1' => [
|
||||
'routes' => base_path('routes/api/v1.php'),
|
||||
'middleware' => [],
|
||||
'status' => 'active',
|
||||
'deprecated_at' => null,
|
||||
'sunset_at' => null,
|
||||
'successor' => null,
|
||||
'documentation' => null,
|
||||
'rate_limit' => null,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Detection Strategy
|
||||
@@ -21,7 +43,7 @@ return [
|
||||
*/
|
||||
'strategies' => [
|
||||
'uri' => [
|
||||
'prefix' => '', // Empty - Laravel 12 adds /api prefix in bootstrap/app.php
|
||||
'prefix' => 'api', // API prefix for versioned routes
|
||||
'pattern' => 'v{version}', // v1, v2, etc.
|
||||
],
|
||||
'header' => [
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
@@ -62,7 +66,7 @@ return [
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
'model' => env('AUTH_MODEL', User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
@@ -59,7 +61,7 @@ return [
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
@@ -79,7 +81,7 @@ return [
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
return [
|
||||
@@ -15,7 +20,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||
'stateful' => explode(',', (string) env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||
'%s%s',
|
||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||
Sanctum::currentApplicationUrlWithPort(),
|
||||
@@ -76,9 +81,9 @@ return [
|
||||
*/
|
||||
|
||||
'middleware' => [
|
||||
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||
'authenticate_session' => AuthenticateSession::class,
|
||||
'encrypt_cookies' => EncryptCookies::class,
|
||||
'validate_csrf_token' => ValidateCsrfToken::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
|
||||
|
||||
return [
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
final class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
private static ?string $password = null;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
@@ -27,7 +30,7 @@ class UserFactory extends Factory
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'password' => self::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
@@ -37,7 +40,7 @@ class UserFactory extends Factory
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
return $this->state(fn (array $attributes): array => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
@@ -11,7 +13,7 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
Schema::create('users', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
@@ -21,13 +23,13 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table): void {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
Schema::create('sessions', function (Blueprint $table): void {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
@@ -11,13 +13,13 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
Schema::create('cache', function (Blueprint $table): void {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
Schema::create('cache_locks', function (Blueprint $table): void {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
@@ -11,7 +13,7 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
Schema::create('jobs', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
@@ -21,7 +23,7 @@ return new class extends Migration
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
Schema::create('job_batches', function (Blueprint $table): void {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
@@ -34,7 +36,7 @@ return new class extends Migration
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
Schema::create('failed_jobs', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
@@ -11,7 +13,7 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->text('name');
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
final class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
|
||||
21
phpstan.neon
Normal file
21
phpstan.neon
Normal file
@@ -0,0 +1,21 @@
|
||||
includes:
|
||||
- vendor/larastan/larastan/extension.neon
|
||||
- vendor/nesbot/carbon/extension.neon
|
||||
- phar://phpstan.phar/conf/bleedingEdge.neon
|
||||
|
||||
parameters:
|
||||
|
||||
paths:
|
||||
- app
|
||||
- bootstrap/app.php
|
||||
- config
|
||||
- database
|
||||
- public
|
||||
- routes
|
||||
|
||||
excludePaths:
|
||||
- config/database.php
|
||||
|
||||
level: max
|
||||
|
||||
tmpDir: /tmp/phpstan
|
||||
63
pint.json
Normal file
63
pint.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"preset": "laravel",
|
||||
"notPath": [
|
||||
"tests/TestCase.php",
|
||||
"tmp",
|
||||
"config/database.php"
|
||||
],
|
||||
"rules": {
|
||||
"array_push": true,
|
||||
"backtick_to_shell_exec": true,
|
||||
"date_time_immutable": true,
|
||||
"declare_strict_types": true,
|
||||
"lowercase_keywords": true,
|
||||
"lowercase_static_reference": true,
|
||||
"final_class": true,
|
||||
"final_internal_class": true,
|
||||
"final_public_method_for_abstract_class": true,
|
||||
"fully_qualified_strict_types": true,
|
||||
"global_namespace_import": {
|
||||
"import_classes": true,
|
||||
"import_constants": true,
|
||||
"import_functions": true
|
||||
},
|
||||
"mb_str_functions": true,
|
||||
"modernize_types_casting": true,
|
||||
"new_with_parentheses": false,
|
||||
"no_superfluous_elseif": true,
|
||||
"no_useless_else": true,
|
||||
"no_multiple_statements_per_line": true,
|
||||
"ordered_class_elements": {
|
||||
"order": [
|
||||
"use_trait",
|
||||
"case",
|
||||
"constant",
|
||||
"constant_public",
|
||||
"constant_protected",
|
||||
"constant_private",
|
||||
"property_public",
|
||||
"property_protected",
|
||||
"property_private",
|
||||
"construct",
|
||||
"destruct",
|
||||
"magic",
|
||||
"phpunit",
|
||||
"method_abstract",
|
||||
"method_public_static",
|
||||
"method_public",
|
||||
"method_protected_static",
|
||||
"method_protected",
|
||||
"method_private_static",
|
||||
"method_private"
|
||||
],
|
||||
"sort_algorithm": "none"
|
||||
},
|
||||
"ordered_interfaces": true,
|
||||
"ordered_traits": true,
|
||||
"protected_to_private": true,
|
||||
"self_accessor": true,
|
||||
"self_static_accessor": true,
|
||||
"strict_comparison": true,
|
||||
"visibility_required": true
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
||||
54
rector.php
Normal file
54
rector.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Rector\Caching\ValueObject\Storage\FileCacheStorage;
|
||||
use Rector\Config\RectorConfig;
|
||||
use Rector\Php83\Rector\ClassMethod\AddOverrideAttributeToOverriddenMethodsRector;
|
||||
use RectorLaravel\Set\LaravelSetList;
|
||||
use RectorLaravel\Set\LaravelSetProvider;
|
||||
|
||||
return RectorConfig::configure()
|
||||
->withSetProviders(LaravelSetProvider::class)
|
||||
->withSets([
|
||||
LaravelSetList::LARAVEL_ARRAYACCESS_TO_METHOD_CALL,
|
||||
LaravelSetList::LARAVEL_ARRAY_STR_FUNCTION_TO_STATIC_CALL,
|
||||
LaravelSetList::LARAVEL_CODE_QUALITY,
|
||||
LaravelSetList::LARAVEL_COLLECTION,
|
||||
LaravelSetList::LARAVEL_CONTAINER_STRING_TO_FULLY_QUALIFIED_NAME,
|
||||
LaravelSetList::LARAVEL_ELOQUENT_MAGIC_METHOD_TO_QUERY_BUILDER,
|
||||
LaravelSetList::LARAVEL_FACADE_ALIASES_TO_FULL_NAMES,
|
||||
LaravelSetList::LARAVEL_FACTORIES,
|
||||
LaravelSetList::LARAVEL_IF_HELPERS,
|
||||
LaravelSetList::LARAVEL_LEGACY_FACTORIES_TO_CLASSES,
|
||||
])
|
||||
->withImportNames(
|
||||
removeUnusedImports: true,
|
||||
)
|
||||
->withComposerBased(laravel: true)
|
||||
->withCache(
|
||||
cacheDirectory: '/tmp/rector',
|
||||
cacheClass: FileCacheStorage::class,
|
||||
)
|
||||
->withPaths([
|
||||
__DIR__.'/app',
|
||||
__DIR__.'/bootstrap/app.php',
|
||||
__DIR__.'/config',
|
||||
__DIR__.'/database',
|
||||
__DIR__.'/public',
|
||||
__DIR__.'/routes',
|
||||
__DIR__.'/tests',
|
||||
])
|
||||
->withSkip([
|
||||
AddOverrideAttributeToOverriddenMethodsRector::class,
|
||||
__DIR__.'/config/database.php',
|
||||
])
|
||||
->withPreparedSets(
|
||||
deadCode: true,
|
||||
codeQuality: true,
|
||||
typeDeclarations: true,
|
||||
privatization: true,
|
||||
earlyReturn: true,
|
||||
codingStyle: true,
|
||||
)
|
||||
->withPhpSets();
|
||||
@@ -1,29 +1,20 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\V1\AuthController;
|
||||
use Grazulex\ApiRoute\Facades\ApiRoute;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| API routes are versioned using grazulex/laravel-apiroute.
|
||||
| API routes are versioned using grazulex/laravel-apiroute v2.x.
|
||||
| Versions are defined in config/apiroute.php and route files are
|
||||
| located in routes/api/{version}.php
|
||||
|
|
||||
| Supports URI path, header, query, and Accept header detection.
|
||||
| See config/apiroute.php for configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
// Version 1 - Current stable version
|
||||
ApiRoute::version('v1', function () {
|
||||
// Public routes
|
||||
Route::post('register', [AuthController::class, 'register'])->name('api.v1.register');
|
||||
Route::post('login', [AuthController::class, 'login'])->name('api.v1.login');
|
||||
|
||||
// Protected routes
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::post('logout', [AuthController::class, 'logout'])->name('api.v1.logout');
|
||||
Route::get('me', [AuthController::class, 'me'])->name('api.v1.me');
|
||||
});
|
||||
})->current();
|
||||
// Routes are now loaded automatically from config/apiroute.php
|
||||
// See routes/api/v1.php for version 1 routes
|
||||
|
||||
27
routes/api/v1.php
Normal file
27
routes/api/v1.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Http\Controllers\Api\V1\AuthController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API V1 Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Routes for API version 1.
|
||||
|
|
||||
*/
|
||||
|
||||
// Public routes with auth rate limiter (5/min - brute force protection)
|
||||
Route::middleware('throttle:auth')->group(function (): void {
|
||||
Route::post('register', [AuthController::class, 'register'])->name('api.v1.register');
|
||||
Route::post('login', [AuthController::class, 'login'])->name('api.v1.login');
|
||||
});
|
||||
|
||||
// Protected routes with authenticated rate limiter (120/min)
|
||||
Route::middleware(['auth:sanctum', 'throttle:authenticated'])->group(function (): void {
|
||||
Route::post('logout', [AuthController::class, 'logout'])->name('api.v1.logout');
|
||||
Route::get('me', [AuthController::class, 'me'])->name('api.v1.me');
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
Artisan::command('inspire', function (): void {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
declare(strict_types=1);
|
||||
|
||||
// Web routes disabled - API only application
|
||||
// Scramble documentation available at /docs/api
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
describe('Registration', function () {
|
||||
it('registers a new user successfully', function () {
|
||||
describe('Registration', function (): void {
|
||||
it('registers a new user successfully', function (): void {
|
||||
$response = $this->postJson('/api/v1/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
@@ -33,7 +35,7 @@ describe('Registration', function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails registration with invalid data', function () {
|
||||
it('fails registration with invalid data', function (): void {
|
||||
$response = $this->postJson('/api/v1/register', [
|
||||
'name' => '',
|
||||
'email' => 'invalid-email',
|
||||
@@ -43,7 +45,7 @@ describe('Registration', function () {
|
||||
$response->assertStatus(422);
|
||||
});
|
||||
|
||||
it('fails registration with duplicate email', function () {
|
||||
it('fails registration with duplicate email', function (): void {
|
||||
User::factory()->create(['email' => 'existing@example.com']);
|
||||
|
||||
$response = $this->postJson('/api/v1/register', [
|
||||
@@ -57,8 +59,8 @@ describe('Registration', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Login', function () {
|
||||
it('logs in with valid credentials', function () {
|
||||
describe('Login', function (): void {
|
||||
it('logs in with valid credentials', function (): void {
|
||||
$user = User::factory()->create([
|
||||
'password' => bcrypt('password123'),
|
||||
]);
|
||||
@@ -83,7 +85,7 @@ describe('Login', function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails login with invalid credentials', function () {
|
||||
it('fails login with invalid credentials', function (): void {
|
||||
$user = User::factory()->create([
|
||||
'password' => bcrypt('password123'),
|
||||
]);
|
||||
@@ -100,7 +102,7 @@ describe('Login', function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails login with non-existent user', function () {
|
||||
it('fails login with non-existent user', function (): void {
|
||||
$response = $this->postJson('/api/v1/login', [
|
||||
'email' => 'nonexistent@example.com',
|
||||
'password' => 'password123',
|
||||
@@ -110,12 +112,12 @@ describe('Login', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logout', function () {
|
||||
it('logs out authenticated user', function () {
|
||||
describe('Logout', function (): void {
|
||||
it('logs out authenticated user', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$token = $user->createToken('test-token')->plainTextToken;
|
||||
|
||||
$response = $this->withHeader('Authorization', "Bearer {$token}")
|
||||
$response = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/v1/logout');
|
||||
|
||||
$response->assertStatus(200)
|
||||
@@ -125,19 +127,19 @@ describe('Logout', function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails logout without authentication', function () {
|
||||
it('fails logout without authentication', function (): void {
|
||||
$response = $this->postJson('/api/v1/logout');
|
||||
|
||||
$response->assertStatus(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Me', function () {
|
||||
it('returns authenticated user data', function () {
|
||||
describe('Me', function (): void {
|
||||
it('returns authenticated user data', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$token = $user->createToken('test-token')->plainTextToken;
|
||||
|
||||
$response = $this->withHeader('Authorization', "Bearer {$token}")
|
||||
$response = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/me');
|
||||
|
||||
$response->assertStatus(200)
|
||||
@@ -155,7 +157,7 @@ describe('Me', function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails without authentication', function () {
|
||||
it('fails without authentication', function (): void {
|
||||
$response = $this->getJson('/api/v1/me');
|
||||
|
||||
$response->assertStatus(401);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
@@ -11,7 +15,7 @@
|
||||
|
|
||||
*/
|
||||
|
||||
pest()->extend(Tests\TestCase::class)
|
||||
pest()->extend(TestCase::class)
|
||||
// ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
->in('Feature');
|
||||
|
||||
@@ -26,9 +30,7 @@ pest()->extend(Tests\TestCase::class)
|
||||
|
|
||||
*/
|
||||
|
||||
expect()->extend('toBeOne', function () {
|
||||
return $this->toBe(1);
|
||||
});
|
||||
expect()->extend('toBeOne', fn () => $this->toBe(1));
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -41,7 +43,7 @@ expect()->extend('toBeOne', function () {
|
||||
|
|
||||
*/
|
||||
|
||||
function something()
|
||||
function something(): void
|
||||
{
|
||||
// ..
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
final class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
|
||||
Reference in New Issue
Block a user