2 Commits

Author SHA1 Message Date
Jean-Marc Strauven
9c1f450c4b chore(release): 2.1.0 2026-01-20 21:00:25 +01:00
Jean-Marc Strauven
f417ec23c2 feat: integrate PHPStan, Rector and Pint code quality tools (#13)
* feat: integrate PHPStan, Rector and Pint code quality tools

- Add larastan, rector, and rector-laravel dev dependencies
- Configure PHPStan at max level with Larastan extension
- Configure Rector with Laravel sets and code quality rules
- Configure Pint with strict rules (final_class, strict_types)
- Add composer scripts: lint, test:lint, test:types, test:unit
- Add GitHub Actions workflow for CI on push/PR
- Apply code style fixes across all files

* docs: add code quality section to README
2026-01-20 20:58:32 +01:00
46 changed files with 737 additions and 83 deletions

55
.github/workflows/tests.yml vendored Normal file
View 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

View File

@@ -2,6 +2,11 @@
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

View File

@@ -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
@@ -435,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

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;

View File

@@ -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');
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
abstract class Controller

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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>

View File

@@ -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.

View File

@@ -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()));
}
}

View File

@@ -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);

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
App\Providers\AppServiceProvider::class,
];

View File

@@ -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": "^4.0",
"pestphp/pest-plugin-laravel": "^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"

282
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "07247fcb148cb2f2dd5ce91bdef3c5e1",
"content-hash": "76745da02f7c3b291785aa3cd83bd614",
"packages": [
{
"name": "brick/math",
@@ -7090,6 +7090,42 @@
],
"time": "2026-01-08T07:23:06+00:00"
},
{
"name": "driftingly/rector-laravel",
"version": "2.1.9",
"source": {
"type": "git",
"url": "https://github.com/driftingly/rector-laravel.git",
"reference": "aee9d4a1d489e7ec484fc79f33137f8ee051b3f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/driftingly/rector-laravel/zipball/aee9d4a1d489e7ec484fc79f33137f8ee051b3f7",
"reference": "aee9d4a1d489e7ec484fc79f33137f8ee051b3f7",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0",
"rector/rector": "^2.2.7",
"webmozart/assert": "^1.11"
},
"type": "rector-extension",
"autoload": {
"psr-4": {
"RectorLaravel\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Rector upgrades rules for Laravel Framework",
"support": {
"issues": "https://github.com/driftingly/rector-laravel/issues",
"source": "https://github.com/driftingly/rector-laravel/tree/2.1.9"
},
"time": "2025-12-25T23:31:36+00:00"
},
{
"name": "fakerphp/faker",
"version": "v1.24.1",
@@ -7336,6 +7372,47 @@
},
"time": "2025-04-30T06:54:44+00:00"
},
{
"name": "iamcal/sql-parser",
"version": "v0.6",
"source": {
"type": "git",
"url": "https://github.com/iamcal/SQLParser.git",
"reference": "947083e2dca211a6f12fb1beb67a01e387de9b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/iamcal/SQLParser/zipball/947083e2dca211a6f12fb1beb67a01e387de9b62",
"reference": "947083e2dca211a6f12fb1beb67a01e387de9b62",
"shasum": ""
},
"require-dev": {
"php-coveralls/php-coveralls": "^1.0",
"phpunit/phpunit": "^5|^6|^7|^8|^9"
},
"type": "library",
"autoload": {
"psr-4": {
"iamcal\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Cal Henderson",
"email": "cal@iamcal.com"
}
],
"description": "MySQL schema parser",
"support": {
"issues": "https://github.com/iamcal/SQLParser/issues",
"source": "https://github.com/iamcal/SQLParser/tree/v0.6"
},
"time": "2025-03-17T16:59:46+00:00"
},
{
"name": "jean85/pretty-package-versions",
"version": "2.1.1",
@@ -7396,6 +7473,96 @@
},
"time": "2025-03-19T14:43:43+00:00"
},
{
"name": "larastan/larastan",
"version": "v3.9.0",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
"reference": "82c18890d0d5b012bc39a3432531e5b6cd1b4b3a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/larastan/larastan/zipball/82c18890d0d5b012bc39a3432531e5b6cd1b4b3a",
"reference": "82c18890d0d5b012bc39a3432531e5b6cd1b4b3a",
"shasum": ""
},
"require": {
"ext-json": "*",
"iamcal/sql-parser": "^0.6.0",
"illuminate/console": "^11.44.2 || ^12.4.1",
"illuminate/container": "^11.44.2 || ^12.4.1",
"illuminate/contracts": "^11.44.2 || ^12.4.1",
"illuminate/database": "^11.44.2 || ^12.4.1",
"illuminate/http": "^11.44.2 || ^12.4.1",
"illuminate/pipeline": "^11.44.2 || ^12.4.1",
"illuminate/support": "^11.44.2 || ^12.4.1",
"php": "^8.2",
"phpstan/phpstan": "^2.1.32"
},
"require-dev": {
"doctrine/coding-standard": "^13",
"laravel/framework": "^11.44.2 || ^12.7.2",
"mockery/mockery": "^1.6.12",
"nikic/php-parser": "^5.4",
"orchestra/canvas": "^v9.2.2 || ^10.0.1",
"orchestra/testbench-core": "^9.12.0 || ^10.1",
"phpstan/phpstan-deprecation-rules": "^2.0.1",
"phpunit/phpunit": "^10.5.35 || ^11.5.15"
},
"suggest": {
"orchestra/testbench": "Using Larastan for analysing a package needs Testbench",
"phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Larastan\\Larastan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Can Vural",
"email": "can9119@gmail.com"
}
],
"description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel",
"keywords": [
"PHPStan",
"code analyse",
"code analysis",
"larastan",
"laravel",
"package",
"php",
"static analysis"
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
"source": "https://github.com/larastan/larastan/tree/v3.9.0"
},
"funding": [
{
"url": "https://github.com/canvural",
"type": "github"
}
],
"time": "2026-01-17T23:00:37+00:00"
},
{
"name": "laravel/pail",
"version": "v1.2.4",
@@ -8304,6 +8471,59 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
{
"name": "phpstan/phpstan",
"version": "2.1.35",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/72f843c7f59d3aac0b7510f5e70913a9b72a8e88",
"reference": "72f843c7f59d3aac0b7510f5e70913a9b72a8e88",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"docs": "https://phpstan.org/user-guide/getting-started",
"forum": "https://github.com/phpstan/phpstan/discussions",
"issues": "https://github.com/phpstan/phpstan/issues",
"security": "https://github.com/phpstan/phpstan/security/policy",
"source": "https://github.com/phpstan/phpstan-src"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
}
],
"time": "2026-01-20T17:33:48+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "12.5.2",
@@ -8743,6 +8963,66 @@
],
"time": "2025-12-15T06:05:34+00:00"
},
{
"name": "rector/rector",
"version": "2.3.2",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
"reference": "07cbbe28bd60251b96b18d42e514779b0e2faa83"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rectorphp/rector/zipball/07cbbe28bd60251b96b18d42e514779b0e2faa83",
"reference": "07cbbe28bd60251b96b18d42e514779b0e2faa83",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0",
"phpstan/phpstan": "^2.1.34"
},
"conflict": {
"rector/rector-doctrine": "*",
"rector/rector-downgrade-php": "*",
"rector/rector-phpunit": "*",
"rector/rector-symfony": "*"
},
"suggest": {
"ext-dom": "To manipulate phpunit.xml via the custom-rule command"
},
"bin": [
"bin/rector"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Instant Upgrade and Automated Refactoring of any PHP code",
"homepage": "https://getrector.com/",
"keywords": [
"automation",
"dev",
"migration",
"refactoring"
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
"source": "https://github.com/rectorphp/rector/tree/2.3.2"
},
"funding": [
{
"url": "https://github.com/tomasvotruba",
"type": "github"
}
],
"time": "2026-01-20T01:11:51+00:00"
},
{
"name": "sebastian/cli-parser",
"version": "4.2.0",

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View File

@@ -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' => [

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Str;
return [

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View File

@@ -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'),
]) : [],
],

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View File

@@ -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,
],
];

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
return [

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Str;
return [

View File

@@ -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,
]);
}

View File

@@ -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();

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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
View 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
View 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
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;

54
rector.php Normal file
View 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();

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/*
|--------------------------------------------------------------------------
| API Routes

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use App\Http\Controllers\Api\V1\AuthController;
use Illuminate\Support\Facades\Route;
@@ -13,13 +15,13 @@ use Illuminate\Support\Facades\Route;
*/
// Public routes with auth rate limiter (5/min - brute force protection)
Route::middleware('throttle:auth')->group(function () {
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 () {
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');
});

View File

@@ -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');

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
// Web routes disabled - API only application
// Scramble documentation available at /docs/api

View File

@@ -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);

View File

@@ -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
{
// ..
}

View File

@@ -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.