From 7f23e347b855225a953c5559995311ce7c9c49d4 Mon Sep 17 00:00:00 2001 From: Jorge Mudry Date: Mon, 30 Mar 2026 20:25:47 -0300 Subject: [PATCH 1/4] Migrate API customizations to Laravel 13 Re-apply all custom API-oriented modifications from master onto a clean Laravel 13 installation. This includes the exception handling pipeline, ForceJsonResponse middleware, health check and Prometheus endpoints, UnGuardedModel with ULIDs, API routing structure, rate limiting, and all dev tooling configs. Skipped bugsnag/bugsnag-laravel (no Laravel 13 support yet). Upgraded treblle to ^6.0.1, Pest to v4, and Tinker to v3 for compatibility. --- app/Models/User.php | 28 +- bootstrap/providers.php | 5 +- composer.json | 63 +- composer.lock | 2242 ++++++++--------- config/app.php | 21 +- config/bugsnag.php | 388 --- config/cache.php | 26 +- config/database.php | 18 +- config/filesystems.php | 2 +- config/logging.php | 4 +- config/mail.php | 4 +- config/queue.php | 19 +- database/factories/UserFactory.php | 3 +- .../0001_01_01_000001_create_cache_table.php | 4 +- ...12_create_personal_access_tokens_table.php | 35 - phpstan.neon | 27 + public/index.php | 1 + resources/views/welcome.blade.php | 277 -- tests/Feature/ExampleTest.php | 9 - tests/Unit/ExampleTest.php | 7 - 20 files changed, 1195 insertions(+), 1988 deletions(-) delete mode 100644 config/bugsnag.php delete mode 100644 database/migrations/2024_07_07_173112_create_personal_access_tokens_table.php delete mode 100644 resources/views/welcome.blade.php delete mode 100644 tests/Feature/ExampleTest.php delete mode 100644 tests/Unit/ExampleTest.php diff --git a/app/Models/User.php b/app/Models/User.php index 81b991b..06290e2 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,38 +5,22 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; +use Database\Factories\UserFactory; +use Illuminate\Database\Eloquent\Attributes\Fillable; +use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +#[Fillable(['name', 'email', 'password'])] +#[Hidden(['password', 'remember_token'])] class User extends Authenticatable { - /** @use HasFactory<\Database\Factories\UserFactory> */ + /** @use HasFactory */ use HasFactory; use Notifiable; - /** - * The attributes that are mass assignable. - * - * @var list - */ - protected $fillable = [ - 'name', - 'email', - 'password', - ]; - - /** - * The attributes that should be hidden for serialization. - * - * @var list - */ - protected $hidden = [ - 'password', - 'remember_token', - ]; - /** * Get the attributes that should be cast. * diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 989328a..888aae0 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,7 +2,8 @@ declare(strict_types=1); +use App\Providers\AppServiceProvider; + return [ - App\Providers\AppServiceProvider::class, - Bugsnag\BugsnagLaravel\BugsnagServiceProvider::class, + AppServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 185dab1..71a28c5 100644 --- a/composer.json +++ b/composer.json @@ -1,30 +1,35 @@ { + "$schema": "https://getcomposer.org/schema.json", "name": "jorgemudry/laravel-api", "type": "project", "description": "A Laravel version to be used on new projects, with some tweaks to make it more API oriented.", - "keywords": ["framework", "laravel", "api"], + "keywords": [ + "framework", + "laravel", + "api" + ], "license": "MIT", "require": { "php": "^8.3", - "bugsnag/bugsnag-laravel": "^2.29.0", "guzzlehttp/guzzle": "^7.10.0", - "laravel/framework": "^12.55.1", + "laravel/framework": "^13.0", "laravel/sanctum": "^4.3.1", - "laravel/tinker": "^2.11.1", + "laravel/tinker": "^3.0", "predis/predis": "^2.4.1", "promphp/prometheus_client_php": "^2.15.0", - "treblle/treblle-laravel": "^4.6.44" + "treblle/treblle-laravel": "^6.0.1" }, "require-dev": { - "fakerphp/faker": "^1.24.1", + "fakerphp/faker": "^1.23", "larastan/larastan": "^3.9.3", - "laravel/pail": "^1.2.6", - "laravel/pint": "^1.29.0", - "mockery/mockery": "^1.6.12", - "nunomaduro/collision": "^8.9.1", + "laravel/boost": "^2.0", + "laravel/pail": "^1.2.5", + "laravel/pint": "^1.27", + "mockery/mockery": "^1.6", + "nunomaduro/collision": "^8.6", "nunomaduro/phpinsights": "^2.14.0", - "pestphp/pest": "^3.8.6", - "pestphp/pest-plugin-laravel": "^3.2", + "pestphp/pest": "^4.4", + "pestphp/pest-plugin-laravel": "^4.1", "rector/rector": "^2.3.9" }, "autoload": { @@ -40,12 +45,32 @@ } }, "scripts": { + "setup": [ + "composer install", + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"", + "@php artisan key:generate", + "@php artisan migrate --force", + "npm install", + "npm run build" + ], + "dev": [ + "Composer\\Config::disableProcessTimeout", + "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others" + ], + "test": [ + "@php artisan config:clear --ansi", + "@php artisan test" + ], "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" ], "post-update-cmd": [ - "@php artisan vendor:publish --tag=laravel-assets --ansi --force" + "@php artisan vendor:publish --tag=laravel-assets --ansi --force", + "@php artisan boost:update --ansi" + ], + "post-install-cmd": [ + "./dev/hooks/install" ], "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" @@ -55,16 +80,8 @@ "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", "@php artisan migrate --graceful --ansi" ], - "post-install-cmd": [ - "./dev/hooks/install" - ], - "dev": [ - "Composer\\Config::disableProcessTimeout", - "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite" - ], - "test": [ - "@php artisan config:clear --ansi", - "@php artisan test" + "pre-package-uninstall": [ + "Illuminate\\Foundation\\ComposerScripts::prePackageUninstall" ] }, "extra": { diff --git a/composer.lock b/composer.lock index 78b9718..695ce1b 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "fa61631f64487ecfdbd8f531282d8016", + "content-hash": "03e4badeeb5d80f9be49f59c96c716a3", "packages": [ { "name": "brick/math", @@ -66,189 +66,6 @@ ], "time": "2026-02-10T14:33:43+00:00" }, - { - "name": "bugsnag/bugsnag", - "version": "v3.30.1", - "source": { - "type": "git", - "url": "https://github.com/bugsnag/bugsnag-php.git", - "reference": "cb2324147efe7aedb5177fdcd68a94f5c5bedf78" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bugsnag/bugsnag-php/zipball/cb2324147efe7aedb5177fdcd68a94f5c5bedf78", - "reference": "cb2324147efe7aedb5177fdcd68a94f5c5bedf78", - "shasum": "" - }, - "require": { - "composer/ca-bundle": "^1.0", - "guzzlehttp/guzzle": "^5.0|^6.0|^7.0", - "php": ">=5.5" - }, - "require-dev": { - "guzzlehttp/psr7": "^1.3|^2.0", - "mtdowling/burgomaster": "dev-master#72151eddf5f0cf101502b94bf5031f9c53501a04", - "php-mock/php-mock-phpunit": "^1.1|^2.1", - "phpunit/phpunit": "^4.8.36|^7.5.15|^9.3.10", - "sebastian/version": ">=1.0.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.20-dev" - } - }, - "autoload": { - "psr-4": { - "Bugsnag\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "James Smith", - "email": "notifiers@bugsnag.com", - "homepage": "https://bugsnag.com" - } - ], - "description": "Official Bugsnag notifier for PHP applications.", - "homepage": "https://github.com/bugsnag/bugsnag-php", - "keywords": [ - "bugsnag", - "errors", - "exceptions", - "logging", - "tracking" - ], - "support": { - "issues": "https://github.com/bugsnag/bugsnag-php/issues", - "source": "https://github.com/bugsnag/bugsnag-php/tree/v3.30.1" - }, - "time": "2026-01-22T16:10:16+00:00" - }, - { - "name": "bugsnag/bugsnag-laravel", - "version": "v2.29.0", - "source": { - "type": "git", - "url": "https://github.com/bugsnag/bugsnag-laravel.git", - "reference": "5066f0aa068f12a555ce26c2d311b133cc06bdb7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bugsnag/bugsnag-laravel/zipball/5066f0aa068f12a555ce26c2d311b133cc06bdb7", - "reference": "5066f0aa068f12a555ce26c2d311b133cc06bdb7", - "shasum": "" - }, - "require": { - "bugsnag/bugsnag": "^3.29.0", - "bugsnag/bugsnag-psr-logger": "^1.4|^2.0", - "illuminate/contracts": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/support": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "monolog/monolog": "^1.12|^2.0|^3.0", - "php": ">=5.5" - }, - "require-dev": { - "orchestra/testbench": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", - "phpunit/phpunit": "^4.8.36|^6.3.1|^7.5.15|^8.3.5|^9.3.10|^10.0|^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.18-dev" - } - }, - "autoload": { - "psr-4": { - "Bugsnag\\BugsnagLaravel\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "James Smith", - "email": "notifiers@bugsnag.com" - } - ], - "description": "Official Bugsnag notifier for Laravel applications.", - "homepage": "https://github.com/bugsnag/bugsnag-laravel", - "keywords": [ - "bugsnag", - "errors", - "exceptions", - "laravel", - "logging", - "tracking" - ], - "support": { - "issues": "https://github.com/bugsnag/bugsnag-laravel/issues", - "source": "https://github.com/bugsnag/bugsnag-laravel/tree/v2.29.0" - }, - "time": "2025-02-26T15:15:21+00:00" - }, - { - "name": "bugsnag/bugsnag-psr-logger", - "version": "v2.0.0", - "source": { - "type": "git", - "url": "https://github.com/bugsnag/bugsnag-psr-logger.git", - "reference": "6c1126e28edcfb7c1da4b492eed883011a09416a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bugsnag/bugsnag-psr-logger/zipball/6c1126e28edcfb7c1da4b492eed883011a09416a", - "reference": "6c1126e28edcfb7c1da4b492eed883011a09416a", - "shasum": "" - }, - "require": { - "bugsnag/bugsnag": "^3.10", - "php": ">=8.0", - "psr/log": "^2.0|^3.0" - }, - "require-dev": { - "graham-campbell/testbench-core": "^1.1", - "mockery/mockery": "^1.3.1", - "phpunit/phpunit": "^9.4.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Bugsnag\\PsrLogger\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "James Smith", - "email": "notifiers@bugsnag.com", - "homepage": "https://bugsnag.com" - } - ], - "description": "Official Bugsnag PHP PSR Logger.", - "homepage": "https://github.com/bugsnag/bugsnag-psr", - "keywords": [ - "bugsnag", - "errors", - "exceptions", - "logging", - "psr", - "tracking" - ], - "support": { - "issues": "https://github.com/bugsnag/bugsnag-psr-logger/issues", - "source": "https://github.com/bugsnag/bugsnag-psr-logger/tree/v2.0.0" - }, - "time": "2022-01-12T11:07:19+00:00" - }, { "name": "carbonphp/carbon-doctrine-types", "version": "3.2.0", @@ -318,78 +135,6 @@ ], "time": "2024-02-09T16:56:22+00:00" }, - { - "name": "composer/ca-bundle", - "version": "1.5.10", - "source": { - "type": "git", - "url": "https://github.com/composer/ca-bundle.git", - "reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/961a5e4056dd2e4a2eedcac7576075947c28bf63", - "reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63", - "shasum": "" - }, - "require": { - "ext-openssl": "*", - "ext-pcre": "*", - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8 || ^9", - "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\CaBundle\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", - "keywords": [ - "cabundle", - "cacert", - "certificate", - "ssl", - "tls" - ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.10" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - } - ], - "time": "2025-12-08T15:06:51+00:00" - }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -1310,24 +1055,24 @@ }, { "name": "laravel/framework", - "version": "v12.55.1", + "version": "v13.2.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "6d9185a248d101b07eecaf8fd60b18129545fd33" + "reference": "9e48d1fe933e89de628dafa167d2c5778566d4cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/6d9185a248d101b07eecaf8fd60b18129545fd33", - "reference": "6d9185a248d101b07eecaf8fd60b18129545fd33", + "url": "https://api.github.com/repos/laravel/framework/zipball/9e48d1fe933e89de628dafa167d2c5778566d4cf", + "reference": "9e48d1fe933e89de628dafa167d2c5778566d4cf", "shasum": "" }, "require": { - "brick/math": "^0.11|^0.12|^0.13|^0.14", + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17", "composer-runtime-api": "^2.2", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.4", - "egulias/email-validator": "^3.2.1|^4.0", + "egulias/email-validator": "^4.0", "ext-ctype": "*", "ext-filter": "*", "ext-hash": "*", @@ -1337,9 +1082,10 @@ "ext-tokenizer": "*", "fruitcake/php-cors": "^1.3", "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/promises": "^2.0.3", "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.3.0", - "laravel/serializable-closure": "^1.3|^2.0", + "laravel/serializable-closure": "^2.0.10", "league/commonmark": "^2.8.1", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", @@ -1347,25 +1093,24 @@ "monolog/monolog": "^3.0", "nesbot/carbon": "^3.8.4", "nunomaduro/termwind": "^2.0", - "php": "^8.2", - "psr/container": "^1.1.1|^2.0.1", - "psr/log": "^1.0|^2.0|^3.0", - "psr/simple-cache": "^1.0|^2.0|^3.0", + "php": "^8.3", + "psr/container": "^1.1.1 || ^2.0.1", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", "ramsey/uuid": "^4.7", - "symfony/console": "^7.2.0", - "symfony/error-handler": "^7.2.0", - "symfony/finder": "^7.2.0", - "symfony/http-foundation": "^7.2.0", - "symfony/http-kernel": "^7.2.0", - "symfony/mailer": "^7.2.0", - "symfony/mime": "^7.2.0", - "symfony/polyfill-php83": "^1.33", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/error-handler": "^7.4.0 || ^8.0.0", + "symfony/finder": "^7.4.0 || ^8.0.0", + "symfony/http-foundation": "^7.4.0 || ^8.0.0", + "symfony/http-kernel": "^7.4.0 || ^8.0.0", + "symfony/mailer": "^7.4.0 || ^8.0.0", + "symfony/mime": "^7.4.0 || ^8.0.0", "symfony/polyfill-php84": "^1.33", "symfony/polyfill-php85": "^1.33", - "symfony/process": "^7.2.0", - "symfony/routing": "^7.2.0", - "symfony/uid": "^7.2.0", - "symfony/var-dumper": "^7.2.0", + "symfony/process": "^7.4.5 || ^8.0.5", + "symfony/routing": "^7.4.0 || ^8.0.0", + "symfony/uid": "^7.4.0 || ^8.0.0", + "symfony/var-dumper": "^7.4.0 || ^8.0.0", "tijsverkoyen/css-to-inline-styles": "^2.2.5", "vlucas/phpdotenv": "^5.6.1", "voku/portable-ascii": "^2.0.2" @@ -1374,9 +1119,9 @@ "tightenco/collect": "<5.5.33" }, "provide": { - "psr/container-implementation": "1.1|2.0", - "psr/log-implementation": "1.0|2.0|3.0", - "psr/simple-cache-implementation": "1.0|2.0|3.0" + "psr/container-implementation": "1.1 || 2.0", + "psr/log-implementation": "1.0 || 2.0 || 3.0", + "psr/simple-cache-implementation": "1.0 || 2.0 || 3.0" }, "replace": { "illuminate/auth": "self.version", @@ -1422,7 +1167,6 @@ "aws/aws-sdk-php": "^3.322.9", "ext-gmp": "*", "fakerphp/faker": "^1.24", - "guzzlehttp/promises": "^2.0.3", "guzzlehttp/psr7": "^2.4", "laravel/pint": "^1.18", "league/flysystem-aws-s3-v3": "^3.25.1", @@ -1432,22 +1176,22 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.9.0", - "pda/pheanstalk": "^5.0.6|^7.0.0", + "orchestra/testbench-core": "^11.0.0", + "pda/pheanstalk": "^7.0.0 || ^8.0.0", "php-http/discovery": "^1.15", - "phpstan/phpstan": "^2.1.41", - "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", - "predis/predis": "^2.3|^3.0", - "resend/resend-php": "^0.10.0|^1.0", - "symfony/cache": "^7.2.0", - "symfony/http-client": "^7.2.0", - "symfony/psr-http-message-bridge": "^7.2.0", - "symfony/translation": "^7.2.0" + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", + "predis/predis": "^2.3 || ^3.0", + "resend/resend-php": "^1.0", + "symfony/cache": "^7.4.0 || ^8.0.0", + "symfony/http-client": "^7.4.0 || ^8.0.0", + "symfony/psr-http-message-bridge": "^7.4.0 || ^8.0.0", + "symfony/translation": "^7.4.0 || ^8.0.0" }, "suggest": { "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", - "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "brianium/paratest": "Required to run tests in parallel (^7.0 || ^8.0).", "ext-apcu": "Required to use the APC cache driver.", "ext-fileinfo": "Required to use the Filesystem class.", "ext-ftp": "Required to use the Flysystem FTP driver.", @@ -1456,7 +1200,7 @@ "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", "ext-pdo": "Required to use all database features.", "ext-posix": "Required to use all features of the queue worker.", - "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0 || ^5.0 || ^6.0).", "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", "laravel/tinker": "Required to use the tinker console command (^2.0).", @@ -1466,24 +1210,24 @@ "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", "mockery/mockery": "Required to use mocking (^1.6).", - "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^7.0 || ^8.0).", "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", - "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", - "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).", + "predis/predis": "Required to use the predis connector (^2.3 || ^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", - "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", - "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", - "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", - "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.4 || ^8.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.4 || ^8.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "12.x-dev" + "dev-master": "13.0.x-dev" } }, "autoload": { @@ -1528,7 +1272,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-03-18T14:28:59+00:00" + "time": "2026-03-24T18:42:09+00:00" }, { "name": "laravel/prompts", @@ -1715,33 +1459,33 @@ }, { "name": "laravel/tinker", - "version": "v2.11.1", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" + "reference": "cc74081282ba2e3dae1f0068ccb330370d24634e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", - "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", + "url": "https://api.github.com/repos/laravel/tinker/zipball/cc74081282ba2e3dae1f0068ccb330370d24634e", + "reference": "cc74081282ba2e3dae1f0068ccb330370d24634e", "shasum": "" }, "require": { - "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "php": "^7.2.5|^8.0", - "psy/psysh": "^0.11.1|^0.12.0", - "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" + "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "psy/psysh": "^0.12.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0" }, "require-dev": { "mockery/mockery": "~1.3.3|^1.4.2", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + "phpunit/phpunit": "^10.5|^11.5" }, "suggest": { - "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + "illuminate/database": "The Illuminate Database package (^8.0|^9.0|^10.0|^11.0|^12.0|^13.0)." }, "type": "library", "extra": { @@ -1749,6 +1493,9 @@ "providers": [ "Laravel\\Tinker\\TinkerServiceProvider" ] + }, + "branch-alias": { + "dev-master": "3.x-dev" } }, "autoload": { @@ -1775,9 +1522,9 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.11.1" + "source": "https://github.com/laravel/tinker/tree/v3.0.0" }, - "time": "2026-02-06T14:12:35+00:00" + "time": "2026-03-17T14:53:17+00:00" }, { "name": "league/commonmark", @@ -3747,22 +3494,21 @@ }, { "name": "symfony/clock", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", - "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", + "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/clock": "^1.0", - "symfony/polyfill-php83": "^1.28" + "php": ">=8.4", + "psr/clock": "^1.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -3801,7 +3547,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.4.0" + "source": "https://github.com/symfony/clock/tree/v8.0.0" }, "funding": [ { @@ -3821,51 +3567,43 @@ "type": "tidelift" } ], - "time": "2025-11-12T15:39:26+00:00" + "time": "2025-11-12T15:46:48+00:00" }, { "name": "symfony/console", - "version": "v7.4.7", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", - "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2|^8.0" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" + "symfony/string": "^7.4|^8.0" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/lock": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3899,7 +3637,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.7" + "source": "https://github.com/symfony/console/tree/v8.0.7" }, "funding": [ { @@ -3919,24 +3657,24 @@ "type": "tidelift" } ], - "time": "2026-03-06T14:06:20+00:00" + "time": "2026-03-06T14:06:22+00:00" }, { "name": "symfony/css-selector", - "version": "v7.4.6", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "2e7c52c647b406e2107dd867db424a4dbac91864" + "reference": "2a178bf80f05dbbe469a337730eba79d61315262" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/2e7c52c647b406e2107dd867db424a4dbac91864", - "reference": "2e7c52c647b406e2107dd867db424a4dbac91864", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/2a178bf80f05dbbe469a337730eba79d61315262", + "reference": "2a178bf80f05dbbe469a337730eba79d61315262", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -3968,7 +3706,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.4.6" + "source": "https://github.com/symfony/css-selector/tree/v8.0.6" }, "funding": [ { @@ -3988,7 +3726,7 @@ "type": "tidelift" } ], - "time": "2026-02-17T07:53:42+00:00" + "time": "2026-02-17T13:07:04+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4059,33 +3797,32 @@ }, { "name": "symfony/error-handler", - "version": "v7.4.4", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" + "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", - "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/7620b97ec0ab1d2d6c7fb737aa55da411bea776a", + "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "psr/log": "^1|^2|^3", "symfony/polyfill-php85": "^1.32", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/var-dumper": "^7.4|^8.0" }, "conflict": { - "symfony/deprecation-contracts": "<2.5", - "symfony/http-kernel": "<6.4" + "symfony/deprecation-contracts": "<2.5" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0", + "symfony/console": "^7.4|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -4117,7 +3854,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.4.4" + "source": "https://github.com/symfony/error-handler/tree/v8.0.4" }, "funding": [ { @@ -4137,28 +3874,28 @@ "type": "tidelift" } ], - "time": "2026-01-20T16:42:42+00:00" + "time": "2026-01-23T11:07:10+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.4.4", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "dc2c0eba1af673e736bb851d747d266108aea746" + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746", - "reference": "dc2c0eba1af673e736bb851d747d266108aea746", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -4167,14 +3904,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/error-handler": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/framework-bundle": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0|^8.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4202,7 +3939,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" }, "funding": [ { @@ -4222,7 +3959,7 @@ "type": "tidelift" } ], - "time": "2026-01-05T11:45:34+00:00" + "time": "2026-01-05T11:45:55+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4302,23 +4039,23 @@ }, { "name": "symfony/finder", - "version": "v7.4.6", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", - "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0|^8.0" + "symfony/filesystem": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4346,7 +4083,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.6" + "source": "https://github.com/symfony/finder/tree/v8.0.6" }, "funding": [ { @@ -4366,41 +4103,39 @@ "type": "tidelift" } ], - "time": "2026-01-29T09:40:50+00:00" + "time": "2026-01-29T09:41:02+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.4.7", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81" + "reference": "c5ecf7b07408dbc4a87482634307654190954ae8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f94b3e7b7dafd40e666f0c9ff2084133bae41e81", - "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/c5ecf7b07408dbc4a87482634307654190954ae8", + "reference": "c5ecf7b07408dbc4a87482634307654190954ae8", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.4", "symfony/polyfill-mbstring": "^1.1" }, "conflict": { - "doctrine/dbal": "<3.6", - "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + "doctrine/dbal": "<4.3" }, "require-dev": { - "doctrine/dbal": "^3.6|^4", + "doctrine/dbal": "^4.3", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5|^8.0", - "symfony/clock": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/mime": "^6.4|^7.0|^8.0", - "symfony/rate-limiter": "^6.4|^7.0|^8.0" + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4428,7 +4163,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.7" + "source": "https://github.com/symfony/http-foundation/tree/v8.0.7" }, "funding": [ { @@ -4448,78 +4183,63 @@ "type": "tidelift" } ], - "time": "2026-03-06T13:15:18+00:00" + "time": "2026-03-06T13:17:40+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.7", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1" + "reference": "c04721f45723d8ce049fa3eee378b5a505272ac7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/3b3fcf386c809be990c922e10e4c620d6367cab1", - "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/c04721f45723d8ce049fa3eee378b5a505272ac7", + "reference": "c04721f45723d8ce049fa3eee378b5a505272ac7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0|^8.0", - "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/browser-kit": "<6.4", - "symfony/cache": "<6.4", - "symfony/config": "<6.4", - "symfony/console": "<6.4", - "symfony/dependency-injection": "<6.4", - "symfony/doctrine-bridge": "<6.4", "symfony/flex": "<2.10", - "symfony/form": "<6.4", - "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/mailer": "<6.4", - "symfony/messenger": "<6.4", - "symfony/translation": "<6.4", "symfony/translation-contracts": "<2.5", - "symfony/twig-bridge": "<6.4", - "symfony/validator": "<6.4", - "symfony/var-dumper": "<6.4", - "twig/twig": "<3.12" + "twig/twig": "<3.21" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0|^8.0", - "symfony/clock": "^6.4|^7.0|^8.0", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/css-selector": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", - "symfony/dom-crawler": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/property-access": "^7.1|^8.0", - "symfony/routing": "^6.4|^7.0|^8.0", - "symfony/serializer": "^7.1|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0", - "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0|^8.0", - "symfony/validator": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0", - "symfony/var-exporter": "^6.4|^7.0|^8.0", - "twig/twig": "^3.12" + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21" }, "type": "library", "autoload": { @@ -4547,7 +4267,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.7" + "source": "https://github.com/symfony/http-kernel/tree/v8.0.7" }, "funding": [ { @@ -4567,43 +4287,39 @@ "type": "tidelift" } ], - "time": "2026-03-06T16:33:18+00:00" + "time": "2026-03-06T16:58:46+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.6", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9" + "reference": "a8971c86b25ff8557e844f08c1f6207d9b3e614c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9", - "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9", + "url": "https://api.github.com/repos/symfony/mailer/zipball/a8971c86b25ff8557e844f08c1f6207d9b3e614c", + "reference": "a8971c86b25ff8557e844f08c1f6207d9b3e614c", "shasum": "" }, "require": { "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=8.2", + "php": ">=8.4", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/mime": "^7.2|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<6.4", - "symfony/messenger": "<6.4", - "symfony/mime": "<6.4", - "symfony/twig-bridge": "<6.4" + "symfony/http-client-contracts": "<2.5" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/http-client": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/twig-bridge": "^6.4|^7.0|^8.0" + "symfony/console": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/twig-bridge": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4631,7 +4347,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.6" + "source": "https://github.com/symfony/mailer/tree/v8.0.6" }, "funding": [ { @@ -4651,44 +4367,41 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-02-25T16:59:43+00:00" }, { "name": "symfony/mime", - "version": "v7.4.7", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1" + "reference": "5d26d1958aeeba2ace8cc64a3a93d4f5d8f8022b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/da5ab4fde3f6c88ab06e96185b9922f48b677cd1", - "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1", + "url": "https://api.github.com/repos/symfony/mime/zipball/5d26d1958aeeba2ace8cc64a3a93d4f5d8f8022b", + "reference": "5d26d1958aeeba2ace8cc64a3a93d4f5d8f8022b", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.4", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<5.2|>=7", - "phpdocumentor/type-resolver": "<1.5.1", - "symfony/mailer": "<6.4", - "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + "phpdocumentor/type-resolver": "<1.5.1" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^5.2|^6.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/property-info": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4720,7 +4433,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.7" + "source": "https://github.com/symfony/mime/tree/v8.0.7" }, "funding": [ { @@ -4740,7 +4453,7 @@ "type": "tidelift" } ], - "time": "2026-03-05T15:24:09+00:00" + "time": "2026-03-06T13:17:40+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5248,86 +4961,6 @@ ], "time": "2025-01-02T08:10:11+00:00" }, - { - "name": "symfony/polyfill-php83", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php83\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-07-08T02:45:35+00:00" - }, { "name": "symfony/polyfill-php84", "version": "v1.33.0", @@ -5573,20 +5206,20 @@ }, { "name": "symfony/process", - "version": "v7.4.5", + "version": "v8.0.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "608476f4604102976d687c483ac63a79ba18cc97" + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", - "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -5614,7 +5247,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.5" + "source": "https://github.com/symfony/process/tree/v8.0.5" }, "funding": [ { @@ -5634,38 +5267,33 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:07:59+00:00" + "time": "2026-01-26T15:08:38+00:00" }, { "name": "symfony/routing", - "version": "v7.4.6", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "238d749c56b804b31a9bf3e26519d93b65a60938" + "reference": "053c40fd46e1d19c5c5a94cada93ce6c3facdd55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938", - "reference": "238d749c56b804b31a9bf3e26519d93b65a60938", + "url": "https://api.github.com/repos/symfony/routing/zipball/053c40fd46e1d19c5c5a94cada93ce6c3facdd55", + "reference": "053c40fd46e1d19c5c5a94cada93ce6c3facdd55", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, - "conflict": { - "symfony/config": "<6.4", - "symfony/dependency-injection": "<6.4", - "symfony/yaml": "<6.4" - }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/yaml": "^6.4|^7.0|^8.0" + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -5699,7 +5327,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.6" + "source": "https://github.com/symfony/routing/tree/v8.0.6" }, "funding": [ { @@ -5719,7 +5347,7 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-02-25T16:59:43+00:00" }, { "name": "symfony/service-contracts", @@ -5810,35 +5438,34 @@ }, { "name": "symfony/string", - "version": "v7.4.6", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "9f209231affa85aa930a5e46e6eb03381424b30b" + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/9f209231affa85aa930a5e46e6eb03381424b30b", - "reference": "9f209231affa85aa930a5e46e6eb03381424b30b", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.33", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1|^8.0", - "symfony/http-client": "^6.4|^7.0|^8.0", - "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0|^8.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -5877,7 +5504,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.6" + "source": "https://github.com/symfony/string/tree/v8.0.6" }, "funding": [ { @@ -5897,38 +5524,31 @@ "type": "tidelift" } ], - "time": "2026-02-09T09:33:46+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "symfony/translation", - "version": "v7.4.6", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "1888cf064399868af3784b9e043240f1d89d25ce" + "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/1888cf064399868af3784b9e043240f1d89d25ce", - "reference": "1888cf064399868af3784b9e043240f1d89d25ce", + "url": "https://api.github.com/repos/symfony/translation/zipball/13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", + "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5.3|^3.3" + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" }, "conflict": { "nikic/php-parser": "<5.0", - "symfony/config": "<6.4", - "symfony/console": "<6.4", - "symfony/dependency-injection": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<6.4", - "symfony/service-contracts": "<2.5", - "symfony/twig-bundle": "<6.4", - "symfony/yaml": "<6.4" + "symfony/service-contracts": "<2.5" }, "provide": { "symfony/translation-implementation": "2.3|3.0" @@ -5936,17 +5556,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/routing": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0|^8.0" + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -5977,7 +5597,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.4.6" + "source": "https://github.com/symfony/translation/tree/v8.0.6" }, "funding": [ { @@ -5997,7 +5617,7 @@ "type": "tidelift" } ], - "time": "2026-02-17T07:53:42+00:00" + "time": "2026-02-17T13:07:04+00:00" }, { "name": "symfony/translation-contracts", @@ -6083,24 +5703,24 @@ }, { "name": "symfony/uid", - "version": "v7.4.4", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" + "reference": "8b81bd3700f5c1913c22a3266a647aa1bb974435" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", - "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", + "url": "https://api.github.com/repos/symfony/uid/zipball/8b81bd3700f5c1913c22a3266a647aa1bb974435", + "reference": "8b81bd3700f5c1913c22a3266a647aa1bb974435", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0" + "symfony/console": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -6137,7 +5757,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.4" + "source": "https://github.com/symfony/uid/tree/v8.0.4" }, "funding": [ { @@ -6157,35 +5777,35 @@ "type": "tidelift" } ], - "time": "2026-01-03T23:30:35+00:00" + "time": "2026-01-03T23:40:55+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.4.6", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" + "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", - "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209", + "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { - "symfony/console": "<6.4" + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -6224,7 +5844,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" + "source": "https://github.com/symfony/var-dumper/tree/v8.0.6" }, "funding": [ { @@ -6244,7 +5864,7 @@ "type": "tidelift" } ], - "time": "2026-02-15T10:53:20+00:00" + "time": "2026-02-15T10:53:29+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -6303,41 +5923,39 @@ }, { "name": "treblle/treblle-laravel", - "version": "4.6.44", + "version": "v6.0.1", "source": { "type": "git", "url": "https://github.com/Treblle/treblle-laravel.git", - "reference": "6194a990346b82842fadb5008e43c0296b137a2c" + "reference": "835166e5363913f2377b1e2ed992ce679035f8fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Treblle/treblle-laravel/zipball/6194a990346b82842fadb5008e43c0296b137a2c", - "reference": "6194a990346b82842fadb5008e43c0296b137a2c", + "url": "https://api.github.com/repos/Treblle/treblle-laravel/zipball/835166e5363913f2377b1e2ed992ce679035f8fd", + "reference": "835166e5363913f2377b1e2ed992ce679035f8fd", "shasum": "" }, "require": { "ext-json": "*", - "php": "^8.1", - "treblle/utils": "^0.4.0" + "ext-mbstring": "*", + "php": "^8.2", + "treblle/treblle-php": "^5.0" }, "require-dev": { - "guzzlehttp/guzzle": "^7.7.0", - "laravel/octane": "^1.5.6", - "laravel/pint": "^1.10.3", - "orchestra/testbench": "^7.0 || ^8.5.9", - "phpstan/phpstan": "^1.10.25" + "laravel/pint": "^1.15", + "orchestra/testbench": "^9.0 || ^10.0 || ^11.0" }, "type": "library", "extra": { "laravel": { "providers": [ - "Treblle\\TreblleServiceProvider" + "Treblle\\Laravel\\TreblleServiceProvider" ] } }, "autoload": { "psr-4": { - "Treblle\\": "src/" + "Treblle\\Laravel\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -6346,14 +5964,14 @@ ], "authors": [ { - "name": "Vedran Cindrić", - "email": "vedran@treblle.com", + "name": "Bhushan Gaikwad", + "email": "bhushan@treblle.com", "homepage": "https://treblle.com/", "role": "Developer" }, { - "name": "Steve McDougall", - "email": "juststevemcd@gmail.com", + "name": "Vedran Cindrić", + "email": "vedran@treblle.com", "homepage": "https://treblle.com/", "role": "Developer" } @@ -6370,36 +5988,36 @@ ], "support": { "issues": "https://github.com/Treblle/treblle-laravel/issues", - "source": "https://github.com/Treblle/treblle-laravel/tree/4.6.44" + "source": "https://github.com/Treblle/treblle-laravel/tree/v6.0.1" }, - "time": "2024-12-15T19:59:14+00:00" + "time": "2025-10-30T13:10:37+00:00" }, { - "name": "treblle/utils", - "version": "0.4.0", + "name": "treblle/treblle-php", + "version": "v5.0.1", "source": { "type": "git", - "url": "https://github.com/Treblle/php-utils.git", - "reference": "8a8d837170acf3fff7b02be18f502f11fc2b36e5" + "url": "https://github.com/Treblle/treblle-php.git", + "reference": "cd41ec2fe092963801acc686ec628ee4d1406d97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Treblle/php-utils/zipball/8a8d837170acf3fff7b02be18f502f11fc2b36e5", - "reference": "8a8d837170acf3fff7b02be18f502f11fc2b36e5", + "url": "https://api.github.com/repos/Treblle/treblle-php/zipball/cd41ec2fe092963801acc686ec628ee4d1406d97", + "reference": "cd41ec2fe092963801acc686ec628ee4d1406d97", "shasum": "" }, "require": { - "php": "^8.1" + "ext-mbstring": "*", + "guzzlehttp/guzzle": "^7.4.5 || ^8.0 || ^9.0", + "php": "^8.2" }, "require-dev": { - "laravel/pint": "^1.7", - "pestphp/pest": "^v2.8.2", - "phpstan/phpstan": "^1.10.25" + "laravel/pint": "^1.15" }, "type": "library", "autoload": { "psr-4": { - "Treblle\\Utils\\": "src/" + "Treblle\\Php\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -6408,27 +6026,33 @@ ], "authors": [ { - "name": "Steve McDougall", - "email": "juststevemcd@gmail.com", - "homepage": "https://www.juststeveking.uk/", + "name": "Bhushan Gaikwad", + "email": "bhushan@treblle.com", + "homepage": "https://treblle.com/", + "role": "Developer" + }, + { + "name": "Vedran Cindrić", + "email": "vedran@treblle.com", + "homepage": "https://treblle.com/", "role": "Developer" } ], - "description": "Treblle PHP SDK utilities.", - "homepage": "https://www.treblle.com/", + "description": "Stay in tune with your APIs", + "homepage": "https://treblle.com/", "keywords": [ "api", "debuging", "documentation", - "laravel", "monitoring", + "phpunit", "treblle" ], "support": { - "issues": "https://github.com/Treblle/php-utils/issues", - "source": "https://github.com/Treblle/php-utils/tree/0.4.0" + "issues": "https://github.com/Treblle/treblle-php/issues", + "source": "https://github.com/Treblle/treblle-php/tree/v5.0.1" }, - "time": "2024-09-26T11:45:45+00:00" + "time": "2026-02-23T12:44:37+00:00" }, { "name": "vlucas/phpdotenv", @@ -6592,16 +6216,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.8.5", + "version": "v7.20.0", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "9b324c8fc319cf9728b581c7a90e1c8f6361c5e5" + "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/9b324c8fc319cf9728b581c7a90e1c8f6361c5e5", - "reference": "9b324c8fc319cf9728b581c7a90e1c8f6361c5e5", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d", "shasum": "" }, "require": { @@ -6611,25 +6235,25 @@ "ext-simplexml": "*", "fidry/cpu-core-counter": "^1.3.0", "jean85/pretty-package-versions": "^2.1.1", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "phpunit/php-code-coverage": "^11.0.12", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-timer": "^7.0.1", - "phpunit/phpunit": "^11.5.46", - "sebastian/environment": "^7.2.1", - "symfony/console": "^6.4.22 || ^7.3.4 || ^8.0.3", - "symfony/process": "^6.4.20 || ^7.3.4 || ^8.0.3" + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", + "phpunit/php-file-iterator": "^6.0.1 || ^7", + "phpunit/php-timer": "^8 || ^9", + "phpunit/phpunit": "^12.5.14 || ^13.0.5", + "sebastian/environment": "^8.0.3 || ^9", + "symfony/console": "^7.4.7 || ^8.0.7", + "symfony/process": "^7.4.5 || ^8.0.5" }, "require-dev": { - "doctrine/coding-standard": "^12.0.0", + "doctrine/coding-standard": "^14.0.0", + "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.33", - "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.11", - "phpstan/phpstan-strict-rules": "^2.0.7", - "squizlabs/php_codesniffer": "^3.13.5", - "symfony/filesystem": "^6.4.13 || ^7.3.2 || ^8.0.1" + "phpstan/phpstan": "^2.1.44", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "symfony/filesystem": "^7.4.6 || ^8.0.6" }, "bin": [ "bin/paratest", @@ -6669,7 +6293,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.8.5" + "source": "https://github.com/paratestphp/paratest/tree/v7.20.0" }, "funding": [ { @@ -6681,7 +6305,7 @@ "type": "paypal" } ], - "time": "2026-01-08T08:02:38+00:00" + "time": "2026-03-29T15:46:14+00:00" }, { "name": "clue/ndjson-react", @@ -7766,6 +7390,145 @@ ], "time": "2026-02-20T12:07:12+00:00" }, + { + "name": "laravel/boost", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/boost.git", + "reference": "f6241df9fd81a86d79a051851177d4ffe3e28506" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/boost/zipball/f6241df9fd81a86d79a051851177d4ffe3e28506", + "reference": "f6241df9fd81a86d79a051851177d4ffe3e28506", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.9", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "laravel/mcp": "^0.5.1|^0.6.0", + "laravel/prompts": "^0.3.10", + "laravel/roster": "^0.5.0", + "php": "^8.2" + }, + "require-dev": { + "laravel/pint": "^1.27.0", + "mockery/mockery": "^1.6.12", + "orchestra/testbench": "^9.15.0|^10.6|^11.0", + "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Boost\\BoostServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Boost\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.", + "homepage": "https://github.com/laravel/boost", + "keywords": [ + "ai", + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/boost/issues", + "source": "https://github.com/laravel/boost" + }, + "time": "2026-03-25T16:37:40+00:00" + }, + { + "name": "laravel/mcp", + "version": "v0.6.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/mcp.git", + "reference": "f822c5eb5beed19adb2e5bfe2f46f8c977ecea42" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/mcp/zipball/f822c5eb5beed19adb2e5bfe2f46f8c977ecea42", + "reference": "f822c5eb5beed19adb2e5bfe2f46f8c977ecea42", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/container": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/http": "^11.45.3|^12.41.1|^13.0", + "illuminate/json-schema": "^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "illuminate/validation": "^11.45.3|^12.41.1|^13.0", + "php": "^8.2" + }, + "require-dev": { + "laravel/pint": "^1.20", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "pestphp/pest": "^3.8.5|^4.3.2", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.2.4" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp" + }, + "providers": [ + "Laravel\\Mcp\\Server\\McpServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Mcp\\": "src/", + "Laravel\\Mcp\\Server\\": "src/Server/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Rapidly build MCP servers for your Laravel applications.", + "homepage": "https://github.com/laravel/mcp", + "keywords": [ + "laravel", + "mcp" + ], + "support": { + "issues": "https://github.com/laravel/mcp/issues", + "source": "https://github.com/laravel/mcp" + }, + "time": "2026-03-19T12:37:13+00:00" + }, { "name": "laravel/pail", "version": "v1.2.6", @@ -7914,6 +7677,67 @@ }, "time": "2026-03-12T15:51:39+00:00" }, + { + "name": "laravel/roster", + "version": "v0.5.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/roster.git", + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/roster/zipball/5089de7615f72f78e831590ff9d0435fed0102bb", + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb", + "shasum": "" + }, + "require": { + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/routing": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/yaml": "^7.2|^8.0" + }, + "require-dev": { + "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.0|^4.1", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Roster\\RosterServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Roster\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Detect packages & approaches in use within a Laravel project", + "homepage": "https://github.com/laravel/roster", + "keywords": [ + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/roster/issues", + "source": "https://github.com/laravel/roster" + }, + "time": "2026-03-05T07:58:43+00:00" + }, { "name": "league/container", "version": "5.2.0", @@ -8333,38 +8157,41 @@ }, { "name": "pestphp/pest", - "version": "v3.8.6", + "version": "v4.4.3", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "8871a6f5ef1de8e7c8dee2a270991449a7b6af73" + "reference": "e6ab897594312728ef2e32d586cb4f6780b1b495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/8871a6f5ef1de8e7c8dee2a270991449a7b6af73", - "reference": "8871a6f5ef1de8e7c8dee2a270991449a7b6af73", + "url": "https://api.github.com/repos/pestphp/pest/zipball/e6ab897594312728ef2e32d586cb4f6780b1b495", + "reference": "e6ab897594312728ef2e32d586cb4f6780b1b495", "shasum": "" }, "require": { - "brianium/paratest": "^7.8.5", + "brianium/paratest": "^7.19.2", "nunomaduro/collision": "^8.9.1", "nunomaduro/termwind": "^2.4.0", - "pestphp/pest-plugin": "^3.0.0", - "pestphp/pest-plugin-arch": "^3.1.1", - "pestphp/pest-plugin-mutate": "^3.0.5", - "php": "^8.2.0", - "phpunit/phpunit": "^11.5.50" + "pestphp/pest-plugin": "^4.0.0", + "pestphp/pest-plugin-arch": "^4.0.0", + "pestphp/pest-plugin-mutate": "^4.0.1", + "pestphp/pest-plugin-profanity": "^4.2.1", + "php": "^8.3.0", + "phpunit/phpunit": "^12.5.14", + "symfony/process": "^7.4.5|^8.0.5" }, "conflict": { - "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">11.5.50", - "sebastian/exporter": "<6.0.0", + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.5.14", + "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { - "pestphp/pest-dev-tools": "^3.4.0", - "pestphp/pest-plugin-type-coverage": "^3.6.1", - "symfony/process": "^7.4.5" + "pestphp/pest-dev-tools": "^4.1.0", + "pestphp/pest-plugin-browser": "^4.3.0", + "pestphp/pest-plugin-type-coverage": "^4.0.3", + "psy/psysh": "^0.12.21" }, "bin": [ "bin/pest" @@ -8390,6 +8217,7 @@ "Pest\\Plugins\\Snapshot", "Pest\\Plugins\\Verbose", "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", "Pest\\Plugins\\Parallel" ] }, @@ -8429,7 +8257,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v3.8.6" + "source": "https://github.com/pestphp/pest/tree/v4.4.3" }, "funding": [ { @@ -8441,34 +8269,34 @@ "type": "github" } ], - "time": "2026-03-10T21:04:33+00:00" + "time": "2026-03-21T13:14:39+00:00" }, { "name": "pestphp/pest-plugin", - "version": "v3.0.0", + "version": "v4.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin.git", - "reference": "e79b26c65bc11c41093b10150c1341cc5cdbea83" + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/e79b26c65bc11c41093b10150c1341cc5cdbea83", - "reference": "e79b26c65bc11c41093b10150c1341cc5cdbea83", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", "shasum": "" }, "require": { "composer-plugin-api": "^2.0.0", "composer-runtime-api": "^2.2.2", - "php": "^8.2" + "php": "^8.3" }, "conflict": { - "pestphp/pest": "<3.0.0" + "pestphp/pest": "<4.0.0" }, "require-dev": { - "composer/composer": "^2.7.9", - "pestphp/pest": "^3.0.0", - "pestphp/pest-dev-tools": "^3.0.0" + "composer/composer": "^2.8.10", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" }, "type": "composer-plugin", "extra": { @@ -8495,7 +8323,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin/tree/v3.0.0" + "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" }, "funding": [ { @@ -8511,30 +8339,30 @@ "type": "patreon" } ], - "time": "2024-09-08T23:21:41+00:00" + "time": "2025-08-20T12:35:58+00:00" }, { "name": "pestphp/pest-plugin-arch", - "version": "v3.1.1", + "version": "v4.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-arch.git", - "reference": "db7bd9cb1612b223e16618d85475c6f63b9c8daa" + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/db7bd9cb1612b223e16618d85475c6f63b9c8daa", - "reference": "db7bd9cb1612b223e16618d85475c6f63b9c8daa", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/25bb17e37920ccc35cbbcda3b00d596aadf3e58d", + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d", "shasum": "" }, "require": { - "pestphp/pest-plugin": "^3.0.0", - "php": "^8.2", - "ta-tikoma/phpunit-architecture-test": "^0.8.4" + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "ta-tikoma/phpunit-architecture-test": "^0.8.5" }, "require-dev": { - "pestphp/pest": "^3.8.1", - "pestphp/pest-dev-tools": "^3.4.0" + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" }, "type": "library", "extra": { @@ -8569,7 +8397,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-arch/tree/v3.1.1" + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.0" }, "funding": [ { @@ -8581,31 +8409,31 @@ "type": "github" } ], - "time": "2025-04-16T22:59:48+00:00" + "time": "2025-08-20T13:10:51+00:00" }, { "name": "pestphp/pest-plugin-laravel", - "version": "v3.2.0", + "version": "v4.1.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-laravel.git", - "reference": "6801be82fd92b96e82dd72e563e5674b1ce365fc" + "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/6801be82fd92b96e82dd72e563e5674b1ce365fc", - "reference": "6801be82fd92b96e82dd72e563e5674b1ce365fc", + "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/3057a36669ff11416cc0dc2b521b3aec58c488d0", + "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0", "shasum": "" }, "require": { - "laravel/framework": "^11.39.1|^12.9.2", - "pestphp/pest": "^3.8.2", - "php": "^8.2.0" + "laravel/framework": "^11.45.2|^12.52.0|^13.0", + "pestphp/pest": "^4.4.1", + "php": "^8.3.0" }, "require-dev": { - "laravel/dusk": "^8.2.13|dev-develop", - "orchestra/testbench": "^9.9.0|^10.2.1", - "pestphp/pest-dev-tools": "^3.4.0" + "laravel/dusk": "^8.3.6", + "orchestra/testbench": "^9.13.0|^10.9.0|^11.0", + "pestphp/pest-dev-tools": "^4.1.0" }, "type": "library", "extra": { @@ -8643,7 +8471,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v3.2.0" + "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v4.1.0" }, "funding": [ { @@ -8655,32 +8483,32 @@ "type": "github" } ], - "time": "2025-04-21T07:40:53+00:00" + "time": "2026-02-21T00:29:45+00:00" }, { "name": "pestphp/pest-plugin-mutate", - "version": "v3.0.5", + "version": "v4.0.1", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-mutate.git", - "reference": "e10dbdc98c9e2f3890095b4fe2144f63a5717e08" + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/e10dbdc98c9e2f3890095b4fe2144f63a5717e08", - "reference": "e10dbdc98c9e2f3890095b4fe2144f63a5717e08", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", "shasum": "" }, "require": { - "nikic/php-parser": "^5.2.0", - "pestphp/pest-plugin": "^3.0.0", - "php": "^8.2", + "nikic/php-parser": "^5.6.1", + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", "psr/simple-cache": "^3.0.0" }, "require-dev": { - "pestphp/pest": "^3.0.8", - "pestphp/pest-dev-tools": "^3.0.0", - "pestphp/pest-plugin-type-coverage": "^3.0.0" + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.0" }, "type": "library", "autoload": { @@ -8693,6 +8521,10 @@ "MIT" ], "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, { "name": "Sandro Gehri", "email": "sandrogehri@gmail.com" @@ -8711,7 +8543,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v3.0.5" + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" }, "funding": [ { @@ -8727,7 +8559,63 @@ "type": "github" } ], - "time": "2024-09-22T07:54:40+00:00" + "time": "2025-08-21T20:19:25+00:00" + }, + { + "name": "pestphp/pest-plugin-profanity", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-profanity.git", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3" + }, + "require-dev": { + "faissaloux/pest-plugin-inside": "^1.9", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Profanity\\Plugin" + ] + } + }, + "autoload": { + "psr-4": { + "Pest\\Profanity\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Profanity Plugin", + "keywords": [ + "framework", + "pest", + "php", + "plugin", + "profanity", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" + }, + "time": "2025-12-08T00:13:17+00:00" }, { "name": "phar-io/manifest", @@ -9133,11 +9021,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.44", + "version": "2.1.45", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218", - "reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f8cdfd9421b7edb7686a2d150a234870464eac70", + "reference": "f8cdfd9421b7edb7686a2d150a234870464eac70", "shasum": "" }, "require": { @@ -9182,20 +9070,20 @@ "type": "github" } ], - "time": "2026-03-25T17:34:21+00:00" + "time": "2026-03-30T13:22:02+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.12", + "version": "12.5.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", - "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", "shasum": "" }, "require": { @@ -9203,18 +9091,17 @@ "ext-libxml": "*", "ext-xmlwriter": "*", "nikic/php-parser": "^5.7.0", - "php": ">=8.2", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-text-template": "^4.0.1", - "sebastian/code-unit-reverse-lookup": "^4.0.1", - "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.1", - "sebastian/lines-of-code": "^3.0.1", - "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.3.1" + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^11.5.46" + "phpunit/phpunit": "^12.5.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -9223,7 +9110,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "11.0.x-dev" + "dev-main": "12.5.x-dev" } }, "autoload": { @@ -9252,7 +9139,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3" }, "funding": [ { @@ -9272,32 +9159,32 @@ "type": "tidelift" } ], - "time": "2025-12-24T07:01:01+00:00" + "time": "2026-02-06T06:01:44+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "5.1.1", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", - "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -9325,7 +9212,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" }, "funding": [ { @@ -9345,28 +9232,28 @@ "type": "tidelift" } ], - "time": "2026-02-02T13:52:54+00:00" + "time": "2026-02-02T14:04:18+00:00" }, { "name": "phpunit/php-invoker", - "version": "5.0.1", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-pcntl": "*" @@ -9374,7 +9261,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -9401,7 +9288,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" }, "funding": [ { @@ -9409,32 +9296,32 @@ "type": "github" } ], - "time": "2024-07-03T05:07:44+00:00" + "time": "2025-02-07T04:58:58+00:00" }, { "name": "phpunit/php-text-template", - "version": "4.0.1", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -9461,7 +9348,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" }, "funding": [ { @@ -9469,32 +9356,32 @@ "type": "github" } ], - "time": "2024-07-03T05:08:43+00:00" + "time": "2025-02-07T04:59:16+00:00" }, { "name": "phpunit/php-timer", - "version": "7.0.1", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -9521,7 +9408,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" }, "funding": [ { @@ -9529,20 +9416,20 @@ "type": "github" } ], - "time": "2024-07-03T05:09:35+00:00" + "time": "2025-02-07T04:59:38+00:00" }, { "name": "phpunit/phpunit", - "version": "11.5.50", + "version": "12.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3" + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", - "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0", + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0", "shasum": "" }, "require": { @@ -9555,34 +9442,31 @@ "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.12", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-invoker": "^5.0.1", - "phpunit/php-text-template": "^4.0.1", - "phpunit/php-timer": "^7.0.1", - "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.3", - "sebastian/comparator": "^6.3.3", - "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.1", - "sebastian/exporter": "^6.3.2", - "sebastian/global-state": "^7.0.2", - "sebastian/object-enumerator": "^6.0.1", - "sebastian/type": "^5.1.3", - "sebastian/version": "^5.0.2", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.3", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.4", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", "staabm/side-effects-detector": "^1.0.5" }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files" - }, "bin": [ "phpunit" ], "type": "library", "extra": { "branch-alias": { - "dev-main": "11.5-dev" + "dev-main": "12.5-dev" } }, "autoload": { @@ -9614,7 +9498,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.50" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14" }, "funding": [ { @@ -9638,7 +9522,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T05:59:18+00:00" + "time": "2026-02-18T12:38:40+00:00" }, { "name": "psr/cache", @@ -10277,28 +10161,28 @@ }, { "name": "sebastian/cli-parser", - "version": "3.0.2", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "4.2-dev" } }, "autoload": { @@ -10322,152 +10206,51 @@ "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - } - ], - "time": "2024-07-03T04:41:36+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", - "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" - }, - "funding": [ + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2025-03-19T07:56:08+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "4.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" - }, - "funding": [ + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" } ], - "time": "2024-07-03T04:45:54+00:00" + "time": "2025-09-14T09:36:45+00:00" }, { "name": "sebastian/comparator", - "version": "6.3.3", + "version": "7.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", - "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/diff": "^6.0", - "sebastian/exporter": "^6.0" + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^11.4" + "phpunit/phpunit": "^12.2" }, "suggest": { "ext-bcmath": "For comparing BcMath\\Number objects" @@ -10475,7 +10258,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.3-dev" + "dev-main": "7.1-dev" } }, "autoload": { @@ -10515,7 +10298,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" }, "funding": [ { @@ -10535,33 +10318,33 @@ "type": "tidelift" } ], - "time": "2026-01-24T09:26:40+00:00" + "time": "2026-01-24T09:28:48+00:00" }, { "name": "sebastian/complexity", - "version": "4.0.1", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", "shasum": "" }, "require": { "nikic/php-parser": "^5.0", - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -10585,7 +10368,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" }, "funding": [ { @@ -10593,33 +10376,33 @@ "type": "github" } ], - "time": "2024-07-03T04:49:50+00:00" + "time": "2025-02-07T04:55:25+00:00" }, { "name": "sebastian/diff", - "version": "6.0.2", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -10652,7 +10435,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" }, "funding": [ { @@ -10660,27 +10443,27 @@ "type": "github" } ], - "time": "2024-07-03T04:53:05+00:00" + "time": "2025-02-07T04:55:46+00:00" }, { "name": "sebastian/environment", - "version": "7.2.1", + "version": "8.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", - "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-posix": "*" @@ -10688,7 +10471,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "7.2-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -10716,7 +10499,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4" }, "funding": [ { @@ -10736,34 +10519,34 @@ "type": "tidelift" } ], - "time": "2025-05-21T11:55:47+00:00" + "time": "2026-03-15T07:05:40+00:00" }, { "name": "sebastian/exporter", - "version": "6.3.2", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", - "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/recursion-context": "^6.0" + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.3-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -10806,7 +10589,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" }, "funding": [ { @@ -10826,35 +10609,35 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:12:51+00:00" + "time": "2025-09-24T06:16:11+00:00" }, { "name": "sebastian/global-state", - "version": "7.0.2", + "version": "8.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", "shasum": "" }, "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -10880,41 +10663,53 @@ "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-07-03T04:57:36+00:00" + "time": "2025-08-29T11:29:25+00:00" }, { "name": "sebastian/lines-of-code", - "version": "3.0.1", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", "shasum": "" }, "require": { "nikic/php-parser": "^5.0", - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -10938,7 +10733,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" }, "funding": [ { @@ -10946,34 +10741,34 @@ "type": "github" } ], - "time": "2024-07-03T04:58:38+00:00" + "time": "2025-02-07T04:57:28+00:00" }, { "name": "sebastian/object-enumerator", - "version": "6.0.1", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", "shasum": "" }, "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -10996,7 +10791,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" }, "funding": [ { @@ -11004,32 +10799,32 @@ "type": "github" } ], - "time": "2024-07-03T05:00:13+00:00" + "time": "2025-02-07T04:57:48+00:00" }, { "name": "sebastian/object-reflector", - "version": "4.0.1", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -11052,7 +10847,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" }, "funding": [ { @@ -11060,32 +10855,32 @@ "type": "github" } ], - "time": "2024-07-03T05:01:32+00:00" + "time": "2025-02-07T04:58:17+00:00" }, { "name": "sebastian/recursion-context", - "version": "6.0.3", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", - "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -11116,7 +10911,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "funding": [ { @@ -11136,32 +10931,32 @@ "type": "tidelift" } ], - "time": "2025-08-13T04:42:22+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { "name": "sebastian/type", - "version": "5.1.3", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", - "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -11185,7 +10980,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" }, "funding": [ { @@ -11205,29 +11000,29 @@ "type": "tidelift" } ], - "time": "2025-08-09T06:55:48+00:00" + "time": "2025-08-09T06:57:12+00:00" }, { "name": "sebastian/version", - "version": "5.0.2", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -11251,7 +11046,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/version/issues", "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" }, "funding": [ { @@ -11259,7 +11054,7 @@ "type": "github" } ], - "time": "2024-10-09T05:16:32+00:00" + "time": "2025-02-07T05:00:38+00:00" }, { "name": "slevomat/coding-standard", @@ -11459,34 +11254,30 @@ }, { "name": "symfony/cache", - "version": "v7.4.7", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "665522ec357540e66c294c08583b40ee576574f0" + "reference": "b7b0f4ce5fb57a8dc061d494639e44e2cf7aa30f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/665522ec357540e66c294c08583b40ee576574f0", - "reference": "665522ec357540e66c294c08583b40ee576574f0", + "url": "https://api.github.com/repos/symfony/cache/zipball/b7b0f4ce5fb57a8dc061d494639e44e2cf7aa30f", + "reference": "b7b0f4ce5fb57a8dc061d494639e44e2cf7aa30f", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^3.6", - "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3", - "symfony/var-exporter": "^6.4|^7.0|^8.0" + "symfony/var-exporter": "^7.4|^8.0" }, "conflict": { - "doctrine/dbal": "<3.6", + "doctrine/dbal": "<4.3", "ext-redis": "<6.1", - "ext-relay": "<0.12.1", - "symfony/dependency-injection": "<6.4", - "symfony/http-kernel": "<6.4", - "symfony/var-dumper": "<6.4" + "ext-relay": "<0.12.1" }, "provide": { "psr/cache-implementation": "2.0|3.0", @@ -11495,16 +11286,16 @@ }, "require-dev": { "cache/integration-tests": "dev-master", - "doctrine/dbal": "^3.6|^4", + "doctrine/dbal": "^4.3", "predis/predis": "^1.1|^2.0", "psr/simple-cache": "^1.0|^2.0|^3.0", - "symfony/clock": "^6.4|^7.0|^8.0", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/filesystem": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -11539,7 +11330,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.4.7" + "source": "https://github.com/symfony/cache/tree/v8.0.7" }, "funding": [ { @@ -11559,7 +11350,7 @@ "type": "tidelift" } ], - "time": "2026-03-06T08:14:57+00:00" + "time": "2026-03-06T13:17:40+00:00" }, { "name": "symfony/cache-contracts", @@ -11639,25 +11430,25 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.6", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e" + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e", - "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0|^8.0" + "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -11685,7 +11476,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.6" + "source": "https://github.com/symfony/filesystem/tree/v8.0.6" }, "funding": [ { @@ -11705,35 +11496,31 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-02-25T16:59:43+00:00" }, { "name": "symfony/http-client", - "version": "v7.4.7", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "1010624285470eb60e88ed10035102c75b4ea6af" + "reference": "ade9bd433450382f0af154661fc8e72758b4de36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/1010624285470eb60e88ed10035102c75b4ea6af", - "reference": "1010624285470eb60e88ed10035102c75b4ea6af", + "url": "https://api.github.com/repos/symfony/http-client/zipball/ade9bd433450382f0af154661fc8e72758b4de36", + "reference": "ade9bd433450382f0af154661fc8e72758b4de36", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "~3.4.4|^3.5.2", - "symfony/polyfill-php83": "^1.29", "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "amphp/amp": "<2.5", - "amphp/socket": "<1.1", - "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.4" + "amphp/amp": "<3", + "php-http/discovery": "<1.15" }, "provide": { "php-http/async-client-implementation": "*", @@ -11742,20 +11529,19 @@ "symfony/http-client-implementation": "3.0" }, "require-dev": { - "amphp/http-client": "^4.2.1|^5.0", - "amphp/http-tunnel": "^1.0|^2.0", + "amphp/http-client": "^5.3.2", + "amphp/http-tunnel": "^2.0", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/cache": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/rate-limiter": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0" + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -11786,7 +11572,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.7" + "source": "https://github.com/symfony/http-client/tree/v8.0.7" }, "funding": [ { @@ -11806,7 +11592,7 @@ "type": "tidelift" } ], - "time": "2026-03-05T11:16:58+00:00" + "time": "2026-03-06T13:17:40+00:00" }, { "name": "symfony/http-client-contracts", @@ -11888,20 +11674,20 @@ }, { "name": "symfony/options-resolver", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "b38026df55197f9e39a44f3215788edf83187b80" + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b38026df55197f9e39a44f3215788edf83187b80", - "reference": "b38026df55197f9e39a44f3215788edf83187b80", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -11935,7 +11721,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.4.0" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" }, "funding": [ { @@ -11955,7 +11741,7 @@ "type": "tidelift" } ], - "time": "2025-11-12T15:39:26+00:00" + "time": "2025-11-12T15:55:31+00:00" }, { "name": "symfony/polyfill-php81", @@ -12039,20 +11825,20 @@ }, { "name": "symfony/stopwatch", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "8a24af0a2e8a872fb745047180649b8418303084" + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/8a24af0a2e8a872fb745047180649b8418303084", - "reference": "8a24af0a2e8a872fb745047180649b8418303084", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/67df1914c6ccd2d7b52f70d40cf2aea02159d942", + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/service-contracts": "^2.5|^3" }, "type": "library", @@ -12081,7 +11867,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.4.0" + "source": "https://github.com/symfony/stopwatch/tree/v8.0.0" }, "funding": [ { @@ -12101,30 +11887,29 @@ "type": "tidelift" } ], - "time": "2025-08-04T07:05:15+00:00" + "time": "2025-08-04T07:36:47+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.4" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -12162,7 +11947,82 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" + "source": "https://github.com/symfony/var-exporter/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-05T18:53:00+00:00" + }, + { + "name": "symfony/yaml", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/5f006c50a981e1630bbb70ad409c5d85f9a716e0", + "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v8.0.6" }, "funding": [ { @@ -12182,7 +12042,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:15:23+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", @@ -12245,23 +12105,23 @@ }, { "name": "theseer/tokenizer", - "version": "1.3.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "type": "library", "autoload": { @@ -12283,7 +12143,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { @@ -12291,7 +12151,7 @@ "type": "github" } ], - "time": "2025-11-17T20:03:58+00:00" + "time": "2025-12-08T11:19:18+00:00" }, { "name": "webmozart/assert", diff --git a/config/app.php b/config/app.php index 6492e4f..7d48fac 100644 --- a/config/app.php +++ b/config/app.php @@ -2,9 +2,6 @@ declare(strict_types=1); -use Illuminate\Session\SessionServiceProvider; -use Illuminate\Support\ServiceProvider; - return [ /* @@ -70,7 +67,7 @@ | */ - 'timezone' => env('APP_TIMEZONE', 'UTC'), + 'timezone' => 'UTC', /* |-------------------------------------------------------------------------- @@ -106,7 +103,7 @@ 'previous_keys' => [ ...array_filter( - explode(',', strval(env('APP_PREVIOUS_KEYS', ''))) + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) ), ], @@ -128,18 +125,4 @@ 'store' => env('APP_MAINTENANCE_STORE', 'database'), ], - 'providers' => ServiceProvider::defaultProviders() - ->except([ - SessionServiceProvider::class, - Illuminate\Cookie\CookieServiceProvider::class, - Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, - ]) // remove this - ->merge([ - // Package Service Providers... - ])->merge([ - // Application Service Providers... - // App\Providers\AppServiceProvider::class, - ])->merge([ - // Added Service Providers (Do not remove this line)... - ])->toArray(), ]; diff --git a/config/bugsnag.php b/config/bugsnag.php deleted file mode 100644 index 31bb745..0000000 --- a/config/bugsnag.php +++ /dev/null @@ -1,388 +0,0 @@ - env('BUGSNAG_API_KEY', ''), - - /* - |-------------------------------------------------------------------------- - | App Type - |-------------------------------------------------------------------------- - | - | Set the type of application executing the current code. - | - */ - - 'app_type' => env('BUGSNAG_APP_TYPE'), - - /* - |-------------------------------------------------------------------------- - | App Version - |-------------------------------------------------------------------------- - | - | Set the version of application executing the current code. - | - */ - - 'app_version' => env('BUGSNAG_APP_VERSION'), - - /* - |-------------------------------------------------------------------------- - | Batch Sending - |-------------------------------------------------------------------------- - | - | Set to true to send the errors through to Bugsnag when the PHP process - | shuts down, in order to prevent your app waiting on HTTP requests. - | - | Setting this to false will send an HTTP request straight away for each - | error. - | - */ - - 'batch_sending' => env('BUGSNAG_BATCH_SENDING'), - - /* - |-------------------------------------------------------------------------- - | Endpoint - |-------------------------------------------------------------------------- - | - | Set what server the Bugsnag notifier should send errors to. By default - | this is set to 'https://notify.bugsnag.com', but for Bugsnag Enterprise - | this should be the URL to your Bugsnag instance. - | - */ - - 'endpoint' => env('BUGSNAG_ENDPOINT'), - - /* - |-------------------------------------------------------------------------- - | Filters - |-------------------------------------------------------------------------- - | - | Use this if you want to ensure you don't send sensitive data such as - | passwords, and credit card numbers to our servers. Any keys which - | contain these strings will be filtered. - | - | This option has been deprecated in favour of 'redacted_keys' - | - */ - - 'filters' => empty(env('BUGSNAG_FILTERS')) - ? null - : explode(',', str_replace(' ', '', strval(env('BUGSNAG_FILTERS')))), - - /* - |-------------------------------------------------------------------------- - | Hostname - |-------------------------------------------------------------------------- - | - | You can set the hostname of your server to something specific for you to - | identify it by if needed. - | - */ - - 'hostname' => env('BUGSNAG_HOSTNAME'), - - /* - |-------------------------------------------------------------------------- - | Proxy - |-------------------------------------------------------------------------- - | - | This is where you can set the proxy settings you'd like us to use when - | communicating with Bugsnag when reporting errors. - | - */ - - 'proxy' => array_filter([ - 'http' => env('HTTP_PROXY'), - 'https' => env('HTTPS_PROXY'), - 'no' => empty(env('NO_PROXY')) - ? null - : explode(',', str_replace(' ', '', strval(env('NO_PROXY')))), - ]), - - /* - |-------------------------------------------------------------------------- - | Project Root - |-------------------------------------------------------------------------- - | - | Bugsnag marks stacktrace lines as in-project if they come from files - | inside your “project root”. You can set this here. - | - | If this is not set, we will automatically try to detect it. - | - */ - - 'project_root' => env('BUGSNAG_PROJECT_ROOT'), - - /* - |-------------------------------------------------------------------------- - | Project Root Regex - |-------------------------------------------------------------------------- - | - | Bugsnag marks stacktrace lines as in-project if they come from files - | inside your “project root”. You can set this here. - | - | This option allows you to set it as a regular expression and will take - | precedence over "project_root" if both are defined. - | - */ - - 'project_root_regex' => env('BUGSNAG_PROJECT_ROOT_REGEX'), - - /* - |-------------------------------------------------------------------------- - | Strip Path - |-------------------------------------------------------------------------- - | - | The strip path is a path to be trimmed from the start of any filepaths in - | your stacktraces. - | - | If this is not set, we will automatically try to detect it. - | - */ - - 'strip_path' => env('BUGSNAG_STRIP_PATH'), - - /* - |-------------------------------------------------------------------------- - | Strip Path Regex - |-------------------------------------------------------------------------- - | - | The strip path is a path to be trimmed from the start of any filepaths in - | your stacktraces. - | - | This option allows you to set it as a regular expression and will take - | precedence over "strip_path" if both are defined. - | - */ - - 'strip_path_regex' => env('BUGSNAG_STRIP_PATH_REGEX'), - - /* - |-------------------------------------------------------------------------- - | Query - |-------------------------------------------------------------------------- - | - | Enable this if you'd like us to automatically record all queries executed - | as breadcrumbs. - | - */ - - 'query' => env('BUGSNAG_QUERY', true), - - /* - |-------------------------------------------------------------------------- - | Bindings - |-------------------------------------------------------------------------- - | - | Enable this if you'd like us to include the query bindings in our query - | breadcrumbs. - | - */ - - 'bindings' => env('BUGSNAG_QUERY_BINDINGS', false), - - /* - |-------------------------------------------------------------------------- - | Release Stage - |-------------------------------------------------------------------------- - | - | Set the release stage to use when sending notifications to Bugsnag. - | - | Leaving this unset will default to using the application environment. - | - */ - - 'release_stage' => env('BUGSNAG_RELEASE_STAGE'), - - /* - |-------------------------------------------------------------------------- - | Notify Release Stages - |-------------------------------------------------------------------------- - | - | Set which release stages should send notifications to Bugsnag. - | - */ - - 'notify_release_stages' => empty(env('BUGSNAG_NOTIFY_RELEASE_STAGES')) - ? null - : explode(',', str_replace(' ', '', strval(env('BUGSNAG_NOTIFY_RELEASE_STAGES')))), - - /* - |-------------------------------------------------------------------------- - | Send Code - |-------------------------------------------------------------------------- - | - | Bugsnag automatically sends a small snippet of the code that crashed to - | help you diagnose even faster from within your dashboard. If you don’t - | want to send this snippet, then set this to false. - | - */ - - 'send_code' => env('BUGSNAG_SEND_CODE', true), - - /* - |-------------------------------------------------------------------------- - | Callbacks - |-------------------------------------------------------------------------- - | - | Enable this if you'd like us to enable our default set of notification - | callbacks. These add things like the cookie information and session - | details to the error to be sent to Bugsnag. - | - | If you'd like to add your own callbacks, you can call the - | Bugsnag::registerCallback method from the boot method of your app - | service provider. - | - */ - - 'callbacks' => env('BUGSNAG_CALLBACKS', true), - - /* - |-------------------------------------------------------------------------- - | User - |-------------------------------------------------------------------------- - | - | Enable this if you'd like us to set the current user logged in via - | Laravel's authentication system. - | - | If you'd like to add your own user resolver, you can do this by using - | callbacks via Bugsnag::registerCallback. - | - */ - - 'user' => env('BUGSNAG_USER', true), - - /* - |-------------------------------------------------------------------------- - | Logger Notify Level - |-------------------------------------------------------------------------- - | - | This sets the level at which a logged message will trigger a notification - | to Bugsnag. By default this level will be 'notice'. - | - | Must be one of the Psr\Log\LogLevel levels from the Psr specification. - | - */ - - 'logger_notify_level' => env('BUGSNAG_LOGGER_LEVEL'), - - /* - |-------------------------------------------------------------------------- - | Auto Capture Sessions - |-------------------------------------------------------------------------- - | - | Enable this to start tracking sessions and deliver them to Bugsnag. - | - */ - - 'auto_capture_sessions' => env('BUGSNAG_CAPTURE_SESSIONS', false), - - /* - |-------------------------------------------------------------------------- - | Sessions Endpoint - |-------------------------------------------------------------------------- - | - | Sets a url to send tracked sessions to. - | - */ - - 'session_endpoint' => env('BUGSNAG_SESSION_ENDPOINT'), - - /* - |-------------------------------------------------------------------------- - | Builds Endpoint - |-------------------------------------------------------------------------- - | - | Sets a url to send build reports to. - | - */ - - 'build_endpoint' => env('BUGSNAG_BUILD_ENDPOINT'), - - /* - |-------------------------------------------------------------------------- - | Discard Classes - |-------------------------------------------------------------------------- - | - | An array of classes that should not be sent to Bugsnag. - | - | This can contain both fully qualified class names and regular expressions. - | - */ - - 'discard_classes' => empty(env('BUGSNAG_DISCARD_CLASSES')) - ? null - : explode(',', strval(env('BUGSNAG_DISCARD_CLASSES'))), - - /* - |-------------------------------------------------------------------------- - | Redacted Keys - |-------------------------------------------------------------------------- - | - | An array of metadata keys that should be redacted. - | - */ - - 'redacted_keys' => empty(env('BUGSNAG_REDACTED_KEYS')) - ? null - : explode(',', strval(env('BUGSNAG_REDACTED_KEYS'))), - - /* - |-------------------------------------------------------------------------- - | Feature flags - |-------------------------------------------------------------------------- - | - | An array of feature flags to add to all reports. - | - | Each element in the array must have a "name" key and can optionally have a - | "variant" key, for example: - | - | [ - | ['name' => 'example without a variant'], - | ['name' => 'example with a variant', 'variant' => 'example of a variant'], - | ] - | - */ - - 'feature_flags' => [], - - /* - |-------------------------------------------------------------------------- - | Max breadcrumbs - |-------------------------------------------------------------------------- - | - | The maximum number of breadcrumbs to send with a report. - | - | This should be an integer between 0-100 (inclusive). - | - */ - - 'max_breadcrumbs' => null, - - /* - |-------------------------------------------------------------------------- - | Attach hidden context - |-------------------------------------------------------------------------- - | - | Whether to attach hidden Context data to events as metadata. - | - */ - - 'attach_hidden_context' => env('BUGSNAG_ATTACH_HIDDEN_CONTEXT', false), -]; diff --git a/config/cache.php b/config/cache.php index 168c390..62c6b58 100644 --- a/config/cache.php +++ b/config/cache.php @@ -29,7 +29,8 @@ | same cache driver to group types of items stored in your caches. | | Supported drivers: "array", "database", "file", "memcached", - | "redis", "dynamodb", "octane", "null" + | "redis", "dynamodb", "octane", + | "failover", "null" | */ @@ -92,6 +93,14 @@ 'driver' => 'octane', ], + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + ], /* @@ -105,6 +114,19 @@ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug(strval(env('APP_NAME', 'laravel')), '_') . '_cache_'), + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')) . '-cache-'), + + /* + |-------------------------------------------------------------------------- + | Serializable Classes + |-------------------------------------------------------------------------- + | + | This value determines the classes that can be unserialized from cache + | storage. By default, no PHP classes will be unserialized from your + | cache to prevent gadget chain attacks if your APP_KEY is leaked. + | + */ + + 'serializable_classes' => false, ]; diff --git a/config/database.php b/config/database.php index d4192bb..df64d23 100644 --- a/config/database.php +++ b/config/database.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Illuminate\Support\Str; +use Pdo\Mysql; return [ @@ -42,6 +43,7 @@ 'busy_timeout' => null, 'journal_mode' => null, 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', ], 'mysql' => [ @@ -60,7 +62,7 @@ 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], @@ -80,7 +82,7 @@ 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], @@ -96,7 +98,7 @@ 'prefix' => '', 'prefix_indexes' => true, 'search_path' => 'public', - 'sslmode' => 'prefer', + 'sslmode' => env('DB_SSLMODE', 'prefer'), ], 'sqlsrv' => [ @@ -149,7 +151,7 @@ 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(strval(env('APP_NAME', 'laravel')), '_') . '_database_'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')) . '-database-'), 'persistent' => env('REDIS_PERSISTENT', false), ], @@ -160,6 +162,10 @@ 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], 'cache' => [ @@ -169,6 +175,10 @@ 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], ], diff --git a/config/filesystems.php b/config/filesystems.php index d4be6ce..075a4f7 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -43,7 +43,7 @@ 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), - 'url' => env('APP_URL') . '/storage', + 'url' => mb_rtrim(strval(env('APP_URL', 'http://localhost')), '/') . '/storage', 'visibility' => 'public', 'throw' => false, 'report' => false, diff --git a/config/logging.php b/config/logging.php index 47fd175..7243435 100644 --- a/config/logging.php +++ b/config/logging.php @@ -56,7 +56,7 @@ 'stack' => [ 'driver' => 'stack', - 'channels' => explode(',', strval(env('LOG_STACK', 'single'))), + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), 'ignore_exceptions' => false, ], @@ -78,7 +78,7 @@ 'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), - 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')), 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), 'level' => env('LOG_LEVEL', 'critical'), 'replace_placeholders' => true, diff --git a/config/mail.php b/config/mail.php index 815c1f5..0b6ab36 100644 --- a/config/mail.php +++ b/config/mail.php @@ -48,7 +48,7 @@ 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), 'timeout' => null, - 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(strval(env('APP_URL', 'http://localhost')), PHP_URL_HOST)), + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), ], 'ses' => [ @@ -114,7 +114,7 @@ 'from' => [ 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), - 'name' => env('MAIL_FROM_NAME', 'Example'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), ], ]; diff --git a/config/queue.php b/config/queue.php index 8829481..629cb73 100644 --- a/config/queue.php +++ b/config/queue.php @@ -26,7 +26,8 @@ | used by your application. An example configuration is provided for | each backend supported by Laravel. You're also free to add more. | - | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" | */ @@ -74,6 +75,22 @@ 'after_commit' => false, ], + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + ], /* diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 63d6747..e85bec6 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -4,12 +4,13 @@ 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 */ class UserFactory extends Factory { diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php index 0f07ecf..e7ffbc3 100644 --- a/database/migrations/0001_01_01_000001_create_cache_table.php +++ b/database/migrations/0001_01_01_000001_create_cache_table.php @@ -16,13 +16,13 @@ public function up(): void Schema::create('cache', function (Blueprint $table): void { $table->string('key')->primary(); $table->mediumText('value'); - $table->integer('expiration'); + $table->bigInteger('expiration')->index(); }); Schema::create('cache_locks', function (Blueprint $table): void { $table->string('key')->primary(); $table->string('owner'); - $table->integer('expiration'); + $table->bigInteger('expiration')->index(); }); } diff --git a/database/migrations/2024_07_07_173112_create_personal_access_tokens_table.php b/database/migrations/2024_07_07_173112_create_personal_access_tokens_table.php deleted file mode 100644 index da0f744..0000000 --- a/database/migrations/2024_07_07_173112_create_personal_access_tokens_table.php +++ /dev/null @@ -1,35 +0,0 @@ -id(); - $table->morphs('tokenable'); - $table->string('name'); - $table->string('token', 64)->unique(); - $table->text('abilities')->nullable(); - $table->timestamp('last_used_at')->nullable(); - $table->timestamp('expires_at')->nullable(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('personal_access_tokens'); - } -}; diff --git a/phpstan.neon b/phpstan.neon index 4c17608..ef8bbbb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -33,3 +33,30 @@ parameters: - message: '#Access to an undefined property PHPUnit\\Framework\\TestCase::.*#' path: *tests/* + reportUnmatched: false + + # Pest arch() expectations use dynamic method chaining + - + message: '#Call to an undefined method Pest\\PendingCalls\\TestCall::expect\(\).#' + path: *tests/Arch/* + + # Pest expectation chain methods + - + message: '#Access to an undefined property Pest\\Mixins\\Expectation.*#' + path: *tests/* + reportUnmatched: false + + # Mockery dynamic method returns + - + message: '#Call to an undefined method Mockery\\ExpectationInterface\|Mockery\\HigherOrderMessage::andReturn\(\).#' + path: *tests/* + + # Mockery mock type coercion + - + message: '#Parameter .* expects .*, Mockery\\MockInterface given.#' + path: *tests/* + + # Nullable return from getPrevious() + - + message: '#Cannot call method getMessage\(\) on Symfony\\Component\\ErrorHandler\\Exception\\FlattenException\|null.#' + path: *tests/* diff --git a/public/index.php b/public/index.php index 3d51e72..7d9e6bf 100644 --- a/public/index.php +++ b/public/index.php @@ -18,4 +18,5 @@ // Bootstrap Laravel and handle the request... /** @var Application $app */ $app = require_once __DIR__ . '/../bootstrap/app.php'; + $app->handleRequest(Request::capture()); diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php deleted file mode 100644 index c893b80..0000000 --- a/resources/views/welcome.blade.php +++ /dev/null @@ -1,277 +0,0 @@ - - - - - - - Laravel - - - - - - - @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) - @vite(['resources/css/app.css', 'resources/js/app.js']) - @else - - @endif - - -
- @if (Route::has('login')) - - @endif -
-
-
-
-

Let's get started

-

Laravel has an incredibly rich ecosystem.
We suggest starting with the following.

- - -
-
- {{-- Laravel Logo --}} - - - - - - - - - - - {{-- Light Mode 12 SVG --}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{-- Dark Mode 12 SVG --}} - -
-
-
-
- - @if (Route::has('login')) - - @endif - - diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php deleted file mode 100644 index 72fb1f7..0000000 --- a/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,9 +0,0 @@ -get('/v1/service/alive'); - - $response->assertStatus(200); -}); diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php deleted file mode 100644 index 5984fd8..0000000 --- a/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,7 +0,0 @@ -toBeTrue(); -}); From 0272523103e0e36c3e1d51735a8d51f7c1829b33 Mon Sep 17 00:00:00 2001 From: Jorge Mudry Date: Mon, 30 Mar 2026 20:31:06 -0300 Subject: [PATCH 2/4] Add comprehensive test suite for API customizations 51 tests covering all custom components: exception handling pipeline, health check endpoints, Prometheus metrics, ForceJsonResponse middleware, API routing, models, enums, traits, and architecture constraints. --- tests/Arch/ArchitectureTest.php | 28 ++++++++++ tests/Feature/ExceptionHandlingTest.php | 46 +++++++++++++++ tests/Feature/ForceJsonResponseTest.php | 13 +++++ tests/Feature/PrometheusMetricsTest.php | 22 ++++++++ tests/Feature/RoutingTest.php | 26 +++++++++ tests/Feature/ServiceAliveTest.php | 23 ++++++++ tests/Feature/ServiceReadyTest.php | 25 +++++++++ tests/Unit/ErrorResponseBuilderTest.php | 32 +++++++++++ tests/Unit/ExceptionMapperTest.php | 74 +++++++++++++++++++++++++ tests/Unit/FlattenExceptionTest.php | 45 +++++++++++++++ tests/Unit/HasApiResponseTest.php | 59 ++++++++++++++++++++ tests/Unit/StatusEnumTest.php | 19 +++++++ tests/Unit/UnGuardedModelTest.php | 18 ++++++ tests/Unit/UserModelTest.php | 32 +++++++++++ tests/Unit/ValidationExceptionTest.php | 34 ++++++++++++ 15 files changed, 496 insertions(+) create mode 100644 tests/Arch/ArchitectureTest.php create mode 100644 tests/Feature/ExceptionHandlingTest.php create mode 100644 tests/Feature/ForceJsonResponseTest.php create mode 100644 tests/Feature/PrometheusMetricsTest.php create mode 100644 tests/Feature/RoutingTest.php create mode 100644 tests/Feature/ServiceAliveTest.php create mode 100644 tests/Feature/ServiceReadyTest.php create mode 100644 tests/Unit/ErrorResponseBuilderTest.php create mode 100644 tests/Unit/ExceptionMapperTest.php create mode 100644 tests/Unit/FlattenExceptionTest.php create mode 100644 tests/Unit/HasApiResponseTest.php create mode 100644 tests/Unit/StatusEnumTest.php create mode 100644 tests/Unit/UnGuardedModelTest.php create mode 100644 tests/Unit/UserModelTest.php create mode 100644 tests/Unit/ValidationExceptionTest.php diff --git a/tests/Arch/ArchitectureTest.php b/tests/Arch/ArchitectureTest.php new file mode 100644 index 0000000..910be9f --- /dev/null +++ b/tests/Arch/ArchitectureTest.php @@ -0,0 +1,28 @@ +expect('App') + ->toUseStrictTypes(); + +arch('v1 controllers are invokable') + ->expect('App\Http\Controllers\V1') + ->toBeInvokable(); + +arch('enums implement ToArrayEnum') + ->expect('App\Enums') + ->toImplement('App\Contracts\ToArrayEnum'); + +arch('no debugging functions in app code') + ->expect(['dd', 'dump', 'ray', 'var_dump', 'print_r']) + ->not->toBeUsed(); + +arch('exceptions extend the correct base classes') + ->expect('App\Exceptions') + ->toExtend('Symfony\Component\HttpKernel\Exception\HttpException') + ->ignoring([ + 'App\Exceptions\ExceptionMapper', + 'App\Exceptions\ErrorResponseBuilder', + 'App\Exceptions\FlattenException', + ]); diff --git a/tests/Feature/ExceptionHandlingTest.php b/tests/Feature/ExceptionHandlingTest.php new file mode 100644 index 0000000..e779171 --- /dev/null +++ b/tests/Feature/ExceptionHandlingTest.php @@ -0,0 +1,46 @@ +get('/v1/non-existent-route') + ->assertStatus(404) + ->assertJsonStructure(['status', 'type', 'error']); +}); + +it('returns error message for 404 errors', function (): void { + $response = $this->get('/v1/non-existent-route'); + $json = $response->json(); + + expect($json['status'])->toBe(404); + expect($json['error'])->toBeString()->not->toBeEmpty(); +}); + +it('does not include debug info when debug is disabled', function (): void { + config(['app.debug' => false]); + + $response = $this->get('/v1/non-existent-route'); + $json = $response->json(); + + expect($json)->not->toHaveKeys(['file', 'line', 'trace']); +}); + +it('includes debug info when debug is enabled', function (): void { + config(['app.debug' => true]); + + $response = $this->get('/v1/non-existent-route'); + $json = $response->json(); + + expect($json)->toHaveKeys(['file', 'line', 'trace']); +}); + +it('returns json even without accept header', function (): void { + $this->get('/v1/non-existent-route', ['Accept' => 'text/html']) + ->assertHeader('Content-Type', 'application/json') + ->assertStatus(404); +}); + +it('returns no web routes', function (): void { + $this->get('/') + ->assertStatus(404); +}); diff --git a/tests/Feature/ForceJsonResponseTest.php b/tests/Feature/ForceJsonResponseTest.php new file mode 100644 index 0000000..617ee89 --- /dev/null +++ b/tests/Feature/ForceJsonResponseTest.php @@ -0,0 +1,13 @@ +get('/v1/service/alive') + ->assertHeader('Content-Type', 'application/json'); +}); + +it('overrides accept header to json', function (): void { + $this->get('/v1/service/alive', ['Accept' => 'text/html']) + ->assertHeader('Content-Type', 'application/json'); +}); diff --git a/tests/Feature/PrometheusMetricsTest.php b/tests/Feature/PrometheusMetricsTest.php new file mode 100644 index 0000000..3980d7c --- /dev/null +++ b/tests/Feature/PrometheusMetricsTest.php @@ -0,0 +1,22 @@ +app->instance(CollectorRegistry::class, new CollectorRegistry(new InMemory())); + + $this->get('/v1/metrics/') + ->assertStatus(200); +}); + +it('returns prometheus text format content type', function (): void { + $this->app->instance(CollectorRegistry::class, new CollectorRegistry(new InMemory())); + + $response = $this->get('/v1/metrics/'); + $content_type = $response->headers->get('Content-Type'); + + expect($content_type)->toContain('text/plain'); +}); diff --git a/tests/Feature/RoutingTest.php b/tests/Feature/RoutingTest.php new file mode 100644 index 0000000..7fb9c72 --- /dev/null +++ b/tests/Feature/RoutingTest.php @@ -0,0 +1,26 @@ +get('/v1/service/alive') + ->assertStatus(200); +}); + +it('has service ready route', function (): void { + $response = $this->get('/v1/service/ready'); + + // Route exists - returns 200 (all ok) or 503 (redis/db down) + expect($response->status())->toBeIn([200, 503]); +}); + +it('has metrics route', function (): void { + $route = app('router')->getRoutes()->getByAction('App\Http\Controllers\V1\PrometheusMetricsController'); + + expect($route)->not->toBeNull(); +}); + +it('does not have web root route', function (): void { + $this->get('/') + ->assertStatus(404); +}); diff --git a/tests/Feature/ServiceAliveTest.php b/tests/Feature/ServiceAliveTest.php new file mode 100644 index 0000000..b4972a8 --- /dev/null +++ b/tests/Feature/ServiceAliveTest.php @@ -0,0 +1,23 @@ +get('/v1/service/alive') + ->assertStatus(200); +}); + +it('returns correct json structure', function (): void { + $this->get('/v1/service/alive') + ->assertJsonStructure(['data' => ['is_alive', 'status']]); +}); + +it('returns is_alive as true', function (): void { + $this->get('/v1/service/alive') + ->assertJson(['data' => ['is_alive' => true, 'status' => 200]]); +}); + +it('returns json content type', function (): void { + $this->get('/v1/service/alive') + ->assertHeader('Content-Type', 'application/json'); +}); diff --git a/tests/Feature/ServiceReadyTest.php b/tests/Feature/ServiceReadyTest.php new file mode 100644 index 0000000..feb859a --- /dev/null +++ b/tests/Feature/ServiceReadyTest.php @@ -0,0 +1,25 @@ +get('/v1/service/ready') + ->assertJsonStructure(['data' => ['is_ready', 'status', 'git']]); +}); + +it('returns git commit hash as string', function (): void { + $response = $this->get('/v1/service/ready'); + $data = $response->json('data'); + + expect($data['git'])->toBeString(); +}); + +it('returns 503 when redis is unavailable', function (): void { + $response = $this->get('/v1/service/ready'); + $data = $response->json('data'); + + // In test environment Redis is typically not running + // so we accept either 200 (all services up) or 503 (service down) + expect($response->status())->toBeIn([200, 503]); + expect($data['is_ready'])->toBeBool(); +}); diff --git a/tests/Unit/ErrorResponseBuilderTest.php b/tests/Unit/ErrorResponseBuilderTest.php new file mode 100644 index 0000000..d63f9ad --- /dev/null +++ b/tests/Unit/ErrorResponseBuilderTest.php @@ -0,0 +1,32 @@ +toBeInstanceOf(ErrorResponseBuilder::class); +}); + +it('builds an exception resource', function (): void { + $exception = FlattenException::createFromThrowable(new RuntimeException('test')); + $resource = ErrorResponseBuilder::fromFlatten($exception)->build(); + + expect($resource)->toBeInstanceOf(ExceptionResource::class); +}); + +it('adds debug header based on flag', function (): void { + $exception = FlattenException::createFromThrowable(new RuntimeException('test')); + + ErrorResponseBuilder::fromFlatten($exception)->build(debug: true); + expect($exception->getHeaders())->toHaveKey('x-app-debug', true); + + $exception2 = FlattenException::createFromThrowable(new RuntimeException('test')); + ErrorResponseBuilder::fromFlatten($exception2)->build(debug: false); + expect($exception2->getHeaders())->toHaveKey('x-app-debug', false); +}); diff --git a/tests/Unit/ExceptionMapperTest.php b/tests/Unit/ExceptionMapperTest.php new file mode 100644 index 0000000..ff861bf --- /dev/null +++ b/tests/Unit/ExceptionMapperTest.php @@ -0,0 +1,74 @@ +map($flatten); + + expect($mapped->getStatusCode())->toBe(404); +}); + +it('maps AuthorizationException to 403', function (): void { + $exception = new AuthorizationException(); + $flatten = FlattenException::createFromThrowable($exception); + $mapped = ExceptionMapper::fromThrowable($exception)->map($flatten); + + expect($mapped->getStatusCode())->toBe(403); +}); + +it('maps AuthenticationException to 401', function (): void { + $exception = new AuthenticationException(); + $flatten = FlattenException::createFromThrowable($exception); + $mapped = ExceptionMapper::fromThrowable($exception)->map($flatten); + + expect($mapped->getStatusCode())->toBe(401); +}); + +it('maps ValidationException to 422', function (): void { + $translator = Mockery::mock(Illuminate\Contracts\Translation\Translator::class); + $translator->shouldReceive('get')->andReturn('The given data was invalid.'); + $validator = Mockery::mock(Validator::class); + $validator->shouldReceive('errors')->andReturn(new Illuminate\Support\MessageBag()); + $validator->shouldReceive('getTranslator')->andReturn($translator); + $exception = new LaravelValidationException($validator); + $flatten = FlattenException::createFromThrowable($exception); + $mapped = ExceptionMapper::fromThrowable($exception)->map($flatten); + + expect($mapped->getStatusCode())->toBe(422); +}); + +it('sets default message when message is empty', function (): void { + $exception = new ModelNotFoundException(); + $flatten = FlattenException::createFromThrowable($exception); + $flatten->setMessage(''); + $mapped = ExceptionMapper::fromThrowable($exception)->map($flatten); + + expect($mapped->getMessage())->toBe('Sorry, the page you are looking for could not be found.'); +}); + +it('preserves existing message when not empty', function (): void { + $exception = new RuntimeException('Custom error message'); + $flatten = FlattenException::createFromThrowable($exception); + $mapped = ExceptionMapper::fromThrowable($exception)->map($flatten); + + expect($mapped->getMessage())->toBe('Custom error message'); +}); + +it('sets generic message for non-404 empty messages', function (): void { + $exception = new AuthenticationException(); + $flatten = FlattenException::createFromThrowable($exception); + $flatten->setMessage(''); + $mapped = ExceptionMapper::fromThrowable($exception)->map($flatten); + + expect($mapped->getMessage())->toBe('Whoops, looks like something went wrong.'); +}); diff --git a/tests/Unit/FlattenExceptionTest.php b/tests/Unit/FlattenExceptionTest.php new file mode 100644 index 0000000..94262f5 --- /dev/null +++ b/tests/Unit/FlattenExceptionTest.php @@ -0,0 +1,45 @@ +getStatusCode())->toBe(500); + expect($flatten->getMessage())->toBe('Something failed'); +}); + +it('creates from http exception with custom status code', function (): void { + $exception = new NotFoundHttpException('Not found'); + $flatten = FlattenException::createFromThrowable($exception); + + expect($flatten->getStatusCode())->toBe(404); +}); + +it('preserves exception message and code', function (): void { + $exception = new RuntimeException('Test message', 42); + $flatten = FlattenException::createFromThrowable($exception); + + expect($flatten->getMessage())->toBe('Test message'); + expect($flatten->getCode())->toBe(42); +}); + +it('stores and retrieves original exception', function (): void { + $exception = new RuntimeException('Original'); + $flatten = FlattenException::createFromThrowable($exception); + + expect($flatten->getOriginalException())->toBe($exception); +}); + +it('handles previous exceptions', function (): void { + $previous = new RuntimeException('Previous'); + $exception = new RuntimeException('Current', 0, $previous); + $flatten = FlattenException::createFromThrowable($exception); + + expect($flatten->getPrevious())->not->toBeNull(); + expect($flatten->getPrevious()->getMessage())->toBe('Previous'); +}); diff --git a/tests/Unit/HasApiResponseTest.php b/tests/Unit/HasApiResponseTest.php new file mode 100644 index 0000000..59f753f --- /dev/null +++ b/tests/Unit/HasApiResponseTest.php @@ -0,0 +1,59 @@ + $content + * @return array + */ + public function testParsePagination(array $content): array + { + return $this->parsePagination($content); + } + }; + + $content = [ + 'data' => [], + 'meta' => [ + 'current_page' => 1, + 'total' => 10, + 'links' => ['first' => '/page/1'], + ], + ]; + + $result = $object->testParsePagination($content); + + expect($result)->toHaveKey('pagination'); + expect($result)->not->toHaveKey('meta'); + expect($result['pagination'])->not->toHaveKey('links'); + expect($result['pagination'])->toHaveKey('current_page', 1); + expect($result['pagination'])->toHaveKey('total', 10); +}); + +it('returns content unchanged when no meta key', function (): void { + $object = new class() + { + use HasApiResponse; + + /** + * @param array $content + * @return array + */ + public function testParsePagination(array $content): array + { + return $this->parsePagination($content); + } + }; + + $content = ['data' => ['item1', 'item2']]; + $result = $object->testParsePagination($content); + + expect($result)->toBe($content); +}); diff --git a/tests/Unit/StatusEnumTest.php b/tests/Unit/StatusEnumTest.php new file mode 100644 index 0000000..5d8b3f1 --- /dev/null +++ b/tests/Unit/StatusEnumTest.php @@ -0,0 +1,19 @@ +value)->toBe('active'); + expect(Status::INACTIVE->value)->toBe('inactive'); +}); + +it('converts to array with all values', function (): void { + expect(Status::toArray())->toBe(['active', 'inactive']); +}); + +it('implements ToArrayEnum interface', function (): void { + expect(Status::ACTIVE)->toBeInstanceOf(ToArrayEnum::class); +}); diff --git a/tests/Unit/UnGuardedModelTest.php b/tests/Unit/UnGuardedModelTest.php new file mode 100644 index 0000000..9bb878a --- /dev/null +++ b/tests/Unit/UnGuardedModelTest.php @@ -0,0 +1,18 @@ +getGuarded())->toBe([]); +}); + +it('uses ulid primary keys', function (): void { + $model = new UnGuardedModel(); + + expect($model->newUniqueId())->toBeString(); + expect(mb_strlen($model->newUniqueId()))->toBe(26); +}); diff --git a/tests/Unit/UserModelTest.php b/tests/Unit/UserModelTest.php new file mode 100644 index 0000000..ac75a88 --- /dev/null +++ b/tests/Unit/UserModelTest.php @@ -0,0 +1,32 @@ +getFillable())->toBe(['name', 'email', 'password']); +}); + +it('has correct hidden attributes', function (): void { + $user = new User(); + + expect($user->getHidden())->toBe(['password', 'remember_token']); +}); + +it('creates user via factory', function (): void { + $user = User::factory()->create(); + + expect($user)->toBeInstanceOf(User::class); + expect($user->name)->toBeString(); + expect($user->email)->toBeString(); +}); + +it('hashes the password automatically', function (): void { + $user = User::factory()->create(['password' => 'secret123']); + + expect($user->password)->not->toBe('secret123'); + expect(password_verify('secret123', $user->password))->toBeTrue(); +}); diff --git a/tests/Unit/ValidationExceptionTest.php b/tests/Unit/ValidationExceptionTest.php new file mode 100644 index 0000000..098660e --- /dev/null +++ b/tests/Unit/ValidationExceptionTest.php @@ -0,0 +1,34 @@ + 'required']); + + expect($exception->getStatusCode())->toBe(422); +}); + +it('json encodes errors in message', function (): void { + $errors = ['email' => 'The email field is required.']; + $exception = new ValidationException($errors); + $decoded = json_decode($exception->getMessage(), true); + + expect($decoded)->toHaveKey('message', 'Some fields failed to pass validation.'); + expect($decoded)->toHaveKey('fields'); + expect($decoded['fields'])->toBe($errors); +}); + +it('adds validation failed header', function (): void { + $exception = new ValidationException(['name' => 'required']); + + expect($exception->getHeaders())->toHaveKey('X-Status-Reason', 'Validation failed.'); +}); + +it('extends ApiException', function (): void { + $exception = new ValidationException([]); + + expect($exception)->toBeInstanceOf(ApiException::class); +}); From ddf739e44e90f6adcf91251191c0cc6c3e6f496f Mon Sep 17 00:00:00 2001 From: Jorge Mudry Date: Mon, 30 Mar 2026 20:45:06 -0300 Subject: [PATCH 3/4] Add AI agent guidelines, Boost config, and update README for Laravel 13 --- AGENTS.md | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++++ GEMINI.md | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 28 ++++------ boost.json | 15 +++++ 5 files changed, 499 insertions(+), 18 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 GEMINI.md create mode 100644 boost.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fc58b42 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,158 @@ + +=== foundation rules === + +# Laravel Boost Guidelines + +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications. + +## Foundational Context + +This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. + +- php - 8.4 +- laravel/framework (LARAVEL) - v13 +- laravel/prompts (PROMPTS) - v0 +- laravel/boost (BOOST) - v2 +- laravel/mcp (MCP) - v0 +- laravel/pail (PAIL) - v1 +- laravel/pint (PINT) - v1 +- pestphp/pest (PEST) - v4 +- phpunit/phpunit (PHPUNIT) - v12 + +## Skills Activation + +This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. + +- `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. +- `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code. + +## Conventions + +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. +- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. +- Check for existing components to reuse before writing a new one. + +## Verification Scripts + +- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important. + +## Application Structure & Architecture + +- Stick to existing directory structure; don't create new base folders without approval. +- Do not change the application's dependencies without approval. + +## Frontend Bundling + +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. + +## Documentation Files + +- You must only create documentation files if explicitly requested by the user. + +## Replies + +- Be concise in your explanations - focus on what's important rather than explaining obvious details. + +=== boost rules === + +# Laravel Boost + +## Tools + +- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads. +- Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker. +- Use `database-schema` to inspect table structure before writing migrations or models. +- Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user. +- Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries. + +## Searching Documentation (IMPORTANT) + +- Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically. +- Pass a `packages` array to scope results when you know which packages are relevant. +- Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first. +- Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`. + +### Search Syntax + +1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit". +2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order. +3. Combine words and phrases for mixed queries: `middleware "rate limit"`. +4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`. + +## Artisan + +- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters. +- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`. +- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory. +- To check environment variables, read the `.env` file directly. + +## Tinker + +- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code. +- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'` + - Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'` + +=== php rules === + +# PHP + +- Always use curly braces for control structures, even for single-line bodies. +- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private. +- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool` +- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`. +- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic. +- Use array shape type definitions in PHPDoc blocks. + +=== herd rules === + +# Laravel Herd + +- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available. +- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start `, `herd php:list`). Run `herd list` to discover all available commands. + +=== laravel/core rules === + +# Do Things the Laravel Way + +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`. +- If you're creating a generic PHP class, use `php artisan make:class`. +- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. + +### Model Creation + +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options. + +## APIs & Eloquent Resources + +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. + +## URL Generation + +- When generating links to other pages, prefer named routes and the `route()` function. + +## Testing + +- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. +- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. +- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +## Vite Error + +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. + +=== pint/core rules === + +# Laravel Pint Code Formatter + +- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. + +=== pest/core rules === + +## Pest + +- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`. +- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`. +- Do NOT delete tests without approval. + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fc58b42 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,158 @@ + +=== foundation rules === + +# Laravel Boost Guidelines + +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications. + +## Foundational Context + +This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. + +- php - 8.4 +- laravel/framework (LARAVEL) - v13 +- laravel/prompts (PROMPTS) - v0 +- laravel/boost (BOOST) - v2 +- laravel/mcp (MCP) - v0 +- laravel/pail (PAIL) - v1 +- laravel/pint (PINT) - v1 +- pestphp/pest (PEST) - v4 +- phpunit/phpunit (PHPUNIT) - v12 + +## Skills Activation + +This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. + +- `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. +- `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code. + +## Conventions + +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. +- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. +- Check for existing components to reuse before writing a new one. + +## Verification Scripts + +- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important. + +## Application Structure & Architecture + +- Stick to existing directory structure; don't create new base folders without approval. +- Do not change the application's dependencies without approval. + +## Frontend Bundling + +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. + +## Documentation Files + +- You must only create documentation files if explicitly requested by the user. + +## Replies + +- Be concise in your explanations - focus on what's important rather than explaining obvious details. + +=== boost rules === + +# Laravel Boost + +## Tools + +- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads. +- Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker. +- Use `database-schema` to inspect table structure before writing migrations or models. +- Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user. +- Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries. + +## Searching Documentation (IMPORTANT) + +- Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically. +- Pass a `packages` array to scope results when you know which packages are relevant. +- Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first. +- Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`. + +### Search Syntax + +1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit". +2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order. +3. Combine words and phrases for mixed queries: `middleware "rate limit"`. +4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`. + +## Artisan + +- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters. +- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`. +- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory. +- To check environment variables, read the `.env` file directly. + +## Tinker + +- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code. +- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'` + - Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'` + +=== php rules === + +# PHP + +- Always use curly braces for control structures, even for single-line bodies. +- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private. +- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool` +- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`. +- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic. +- Use array shape type definitions in PHPDoc blocks. + +=== herd rules === + +# Laravel Herd + +- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available. +- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start `, `herd php:list`). Run `herd list` to discover all available commands. + +=== laravel/core rules === + +# Do Things the Laravel Way + +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`. +- If you're creating a generic PHP class, use `php artisan make:class`. +- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. + +### Model Creation + +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options. + +## APIs & Eloquent Resources + +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. + +## URL Generation + +- When generating links to other pages, prefer named routes and the `route()` function. + +## Testing + +- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. +- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. +- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +## Vite Error + +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. + +=== pint/core rules === + +# Laravel Pint Code Formatter + +- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. + +=== pest/core rules === + +## Pest + +- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`. +- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`. +- Do NOT delete tests without approval. + + diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..fc58b42 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,158 @@ + +=== foundation rules === + +# Laravel Boost Guidelines + +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications. + +## Foundational Context + +This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. + +- php - 8.4 +- laravel/framework (LARAVEL) - v13 +- laravel/prompts (PROMPTS) - v0 +- laravel/boost (BOOST) - v2 +- laravel/mcp (MCP) - v0 +- laravel/pail (PAIL) - v1 +- laravel/pint (PINT) - v1 +- pestphp/pest (PEST) - v4 +- phpunit/phpunit (PHPUNIT) - v12 + +## Skills Activation + +This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. + +- `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. +- `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code. + +## Conventions + +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. +- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. +- Check for existing components to reuse before writing a new one. + +## Verification Scripts + +- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important. + +## Application Structure & Architecture + +- Stick to existing directory structure; don't create new base folders without approval. +- Do not change the application's dependencies without approval. + +## Frontend Bundling + +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. + +## Documentation Files + +- You must only create documentation files if explicitly requested by the user. + +## Replies + +- Be concise in your explanations - focus on what's important rather than explaining obvious details. + +=== boost rules === + +# Laravel Boost + +## Tools + +- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads. +- Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker. +- Use `database-schema` to inspect table structure before writing migrations or models. +- Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user. +- Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries. + +## Searching Documentation (IMPORTANT) + +- Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically. +- Pass a `packages` array to scope results when you know which packages are relevant. +- Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first. +- Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`. + +### Search Syntax + +1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit". +2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order. +3. Combine words and phrases for mixed queries: `middleware "rate limit"`. +4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`. + +## Artisan + +- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters. +- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`. +- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory. +- To check environment variables, read the `.env` file directly. + +## Tinker + +- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code. +- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'` + - Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'` + +=== php rules === + +# PHP + +- Always use curly braces for control structures, even for single-line bodies. +- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private. +- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool` +- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`. +- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic. +- Use array shape type definitions in PHPDoc blocks. + +=== herd rules === + +# Laravel Herd + +- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available. +- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start `, `herd php:list`). Run `herd list` to discover all available commands. + +=== laravel/core rules === + +# Do Things the Laravel Way + +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`. +- If you're creating a generic PHP class, use `php artisan make:class`. +- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. + +### Model Creation + +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options. + +## APIs & Eloquent Resources + +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. + +## URL Generation + +- When generating links to other pages, prefer named routes and the `route()` function. + +## Testing + +- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. +- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. +- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +## Vite Error + +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. + +=== pint/core rules === + +# Laravel Pint Code Formatter + +- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. + +=== pest/core rules === + +## Pest + +- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`. +- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`. +- Do NOT delete tests without approval. + + diff --git a/README.md b/README.md index 3ed385a..5ad1377 100644 --- a/README.md +++ b/README.md @@ -25,29 +25,21 @@ Laravel is accessible, powerful, and provides tools required for large, robust a Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. -You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. +In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. -If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 2000 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. +You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals. -## Laravel Sponsors +## Agentic Development -We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell). +Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow: -### Premium Partners +```bash +composer require laravel/boost --dev -- **[Vehikl](https://vehikl.com/)** -- **[Tighten Co.](https://tighten.co)** -- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** -- **[64 Robots](https://64robots.com)** -- **[Cubet Techno Labs](https://cubettech.com)** -- **[Cyber-Duck](https://cyber-duck.co.uk)** -- **[Many](https://www.many.co.uk)** -- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)** -- **[DevSquad](https://devsquad.com)** -- **[Curotec](https://www.curotec.com/services/technologies/laravel/)** -- **[OP.GG](https://op.gg)** -- **[WebReinvent](https://webreinvent.com/?utm_source=laravel&utm_medium=github&utm_campaign=patreon-sponsors)** -- **[Lendio](https://lendio.com)** +php artisan boost:install +``` + +Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices. ## Contributing diff --git a/boost.json b/boost.json new file mode 100644 index 0000000..2bc5614 --- /dev/null +++ b/boost.json @@ -0,0 +1,15 @@ +{ + "agents": [ + "junie", + "claude_code", + "gemini" + ], + "guidelines": true, + "mcp": true, + "nightwatch_mcp": false, + "sail": false, + "skills": [ + "laravel-best-practices", + "pest-testing" + ] +} From 0442b9c410f9c6442cff5b5ad9af6c33983c15b5 Mon Sep 17 00:00:00 2001 From: Jorge Mudry Date: Mon, 30 Mar 2026 20:47:40 -0300 Subject: [PATCH 4/4] Add AI agent skills, Boost config, and update dependencies for Laravel 13 --- .../skills/laravel-best-practices/SKILL.md | 190 ++++++++++++++++ .../rules/advanced-queries.md | 106 +++++++++ .../rules/architecture.md | 202 ++++++++++++++++++ .../rules/blade-views.md | 36 ++++ .../laravel-best-practices/rules/caching.md | 70 ++++++ .../rules/collections.md | 44 ++++ .../laravel-best-practices/rules/config.md | 73 +++++++ .../rules/db-performance.md | 192 +++++++++++++++++ .../laravel-best-practices/rules/eloquent.md | 148 +++++++++++++ .../rules/error-handling.md | 72 +++++++ .../rules/events-notifications.md | 48 +++++ .../rules/http-client.md | 160 ++++++++++++++ .../laravel-best-practices/rules/mail.md | 27 +++ .../rules/migrations.md | 121 +++++++++++ .../rules/queue-jobs.md | 146 +++++++++++++ .../laravel-best-practices/rules/routing.md | 98 +++++++++ .../rules/scheduling.md | 39 ++++ .../laravel-best-practices/rules/security.md | 198 +++++++++++++++++ .../laravel-best-practices/rules/style.md | Bin 0 -> 4443 bytes .../laravel-best-practices/rules/testing.md | 43 ++++ .../rules/validation.md | 75 +++++++ .agents/skills/pest-testing/SKILL.md | 157 ++++++++++++++ .../skills/laravel-best-practices/SKILL.md | 190 ++++++++++++++++ .../rules/advanced-queries.md | 106 +++++++++ .../rules/architecture.md | 202 ++++++++++++++++++ .../rules/blade-views.md | 36 ++++ .../laravel-best-practices/rules/caching.md | 70 ++++++ .../rules/collections.md | 44 ++++ .../laravel-best-practices/rules/config.md | 73 +++++++ .../rules/db-performance.md | 192 +++++++++++++++++ .../laravel-best-practices/rules/eloquent.md | 148 +++++++++++++ .../rules/error-handling.md | 72 +++++++ .../rules/events-notifications.md | 48 +++++ .../rules/http-client.md | 160 ++++++++++++++ .../laravel-best-practices/rules/mail.md | 27 +++ .../rules/migrations.md | 121 +++++++++++ .../rules/queue-jobs.md | 146 +++++++++++++ .../laravel-best-practices/rules/routing.md | 98 +++++++++ .../rules/scheduling.md | 39 ++++ .../laravel-best-practices/rules/security.md | 198 +++++++++++++++++ .../laravel-best-practices/rules/style.md | Bin 0 -> 4443 bytes .../laravel-best-practices/rules/testing.md | 43 ++++ .../rules/validation.md | 75 +++++++ .claude/skills/pest-testing/SKILL.md | 157 ++++++++++++++ .github/workflows/static-analysis.yml | 2 +- .github/workflows/tests.yml | 2 +- .junie/skills/laravel-best-practices/SKILL.md | 190 ++++++++++++++++ .../rules/advanced-queries.md | 106 +++++++++ .../rules/architecture.md | 202 ++++++++++++++++++ .../rules/blade-views.md | 36 ++++ .../laravel-best-practices/rules/caching.md | 70 ++++++ .../rules/collections.md | 44 ++++ .../laravel-best-practices/rules/config.md | 73 +++++++ .../rules/db-performance.md | 192 +++++++++++++++++ .../laravel-best-practices/rules/eloquent.md | 148 +++++++++++++ .../rules/error-handling.md | 72 +++++++ .../rules/events-notifications.md | 48 +++++ .../rules/http-client.md | 160 ++++++++++++++ .../laravel-best-practices/rules/mail.md | 27 +++ .../rules/migrations.md | 121 +++++++++++ .../rules/queue-jobs.md | 146 +++++++++++++ .../laravel-best-practices/rules/routing.md | 98 +++++++++ .../rules/scheduling.md | 39 ++++ .../laravel-best-practices/rules/security.md | 198 +++++++++++++++++ .../laravel-best-practices/rules/style.md | Bin 0 -> 4443 bytes .../laravel-best-practices/rules/testing.md | 43 ++++ .../rules/validation.md | 75 +++++++ .junie/skills/pest-testing/SKILL.md | 157 ++++++++++++++ AGENTS.md | 11 +- CLAUDE.md | 11 +- GEMINI.md | 11 +- composer.json | 18 +- composer.lock | 4 +- 73 files changed, 6769 insertions(+), 25 deletions(-) create mode 100644 .agents/skills/laravel-best-practices/SKILL.md create mode 100644 .agents/skills/laravel-best-practices/rules/advanced-queries.md create mode 100644 .agents/skills/laravel-best-practices/rules/architecture.md create mode 100644 .agents/skills/laravel-best-practices/rules/blade-views.md create mode 100644 .agents/skills/laravel-best-practices/rules/caching.md create mode 100644 .agents/skills/laravel-best-practices/rules/collections.md create mode 100644 .agents/skills/laravel-best-practices/rules/config.md create mode 100644 .agents/skills/laravel-best-practices/rules/db-performance.md create mode 100644 .agents/skills/laravel-best-practices/rules/eloquent.md create mode 100644 .agents/skills/laravel-best-practices/rules/error-handling.md create mode 100644 .agents/skills/laravel-best-practices/rules/events-notifications.md create mode 100644 .agents/skills/laravel-best-practices/rules/http-client.md create mode 100644 .agents/skills/laravel-best-practices/rules/mail.md create mode 100644 .agents/skills/laravel-best-practices/rules/migrations.md create mode 100644 .agents/skills/laravel-best-practices/rules/queue-jobs.md create mode 100644 .agents/skills/laravel-best-practices/rules/routing.md create mode 100644 .agents/skills/laravel-best-practices/rules/scheduling.md create mode 100644 .agents/skills/laravel-best-practices/rules/security.md create mode 100644 .agents/skills/laravel-best-practices/rules/style.md create mode 100644 .agents/skills/laravel-best-practices/rules/testing.md create mode 100644 .agents/skills/laravel-best-practices/rules/validation.md create mode 100644 .agents/skills/pest-testing/SKILL.md create mode 100644 .claude/skills/laravel-best-practices/SKILL.md create mode 100644 .claude/skills/laravel-best-practices/rules/advanced-queries.md create mode 100644 .claude/skills/laravel-best-practices/rules/architecture.md create mode 100644 .claude/skills/laravel-best-practices/rules/blade-views.md create mode 100644 .claude/skills/laravel-best-practices/rules/caching.md create mode 100644 .claude/skills/laravel-best-practices/rules/collections.md create mode 100644 .claude/skills/laravel-best-practices/rules/config.md create mode 100644 .claude/skills/laravel-best-practices/rules/db-performance.md create mode 100644 .claude/skills/laravel-best-practices/rules/eloquent.md create mode 100644 .claude/skills/laravel-best-practices/rules/error-handling.md create mode 100644 .claude/skills/laravel-best-practices/rules/events-notifications.md create mode 100644 .claude/skills/laravel-best-practices/rules/http-client.md create mode 100644 .claude/skills/laravel-best-practices/rules/mail.md create mode 100644 .claude/skills/laravel-best-practices/rules/migrations.md create mode 100644 .claude/skills/laravel-best-practices/rules/queue-jobs.md create mode 100644 .claude/skills/laravel-best-practices/rules/routing.md create mode 100644 .claude/skills/laravel-best-practices/rules/scheduling.md create mode 100644 .claude/skills/laravel-best-practices/rules/security.md create mode 100644 .claude/skills/laravel-best-practices/rules/style.md create mode 100644 .claude/skills/laravel-best-practices/rules/testing.md create mode 100644 .claude/skills/laravel-best-practices/rules/validation.md create mode 100644 .claude/skills/pest-testing/SKILL.md create mode 100644 .junie/skills/laravel-best-practices/SKILL.md create mode 100644 .junie/skills/laravel-best-practices/rules/advanced-queries.md create mode 100644 .junie/skills/laravel-best-practices/rules/architecture.md create mode 100644 .junie/skills/laravel-best-practices/rules/blade-views.md create mode 100644 .junie/skills/laravel-best-practices/rules/caching.md create mode 100644 .junie/skills/laravel-best-practices/rules/collections.md create mode 100644 .junie/skills/laravel-best-practices/rules/config.md create mode 100644 .junie/skills/laravel-best-practices/rules/db-performance.md create mode 100644 .junie/skills/laravel-best-practices/rules/eloquent.md create mode 100644 .junie/skills/laravel-best-practices/rules/error-handling.md create mode 100644 .junie/skills/laravel-best-practices/rules/events-notifications.md create mode 100644 .junie/skills/laravel-best-practices/rules/http-client.md create mode 100644 .junie/skills/laravel-best-practices/rules/mail.md create mode 100644 .junie/skills/laravel-best-practices/rules/migrations.md create mode 100644 .junie/skills/laravel-best-practices/rules/queue-jobs.md create mode 100644 .junie/skills/laravel-best-practices/rules/routing.md create mode 100644 .junie/skills/laravel-best-practices/rules/scheduling.md create mode 100644 .junie/skills/laravel-best-practices/rules/security.md create mode 100644 .junie/skills/laravel-best-practices/rules/style.md create mode 100644 .junie/skills/laravel-best-practices/rules/testing.md create mode 100644 .junie/skills/laravel-best-practices/rules/validation.md create mode 100644 .junie/skills/pest-testing/SKILL.md diff --git a/.agents/skills/laravel-best-practices/SKILL.md b/.agents/skills/laravel-best-practices/SKILL.md new file mode 100644 index 0000000..99018f3 --- /dev/null +++ b/.agents/skills/laravel-best-practices/SKILL.md @@ -0,0 +1,190 @@ +--- +name: laravel-best-practices +description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns." +license: MIT +metadata: + author: laravel +--- + +# Laravel Best Practices + +Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`. + +## Consistency First + +Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern. + +Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides. + +## Quick Reference + +### 1. Database Performance → `rules/db-performance.md` + +- Eager load with `with()` to prevent N+1 queries +- Enable `Model::preventLazyLoading()` in development +- Select only needed columns, avoid `SELECT *` +- `chunk()` / `chunkById()` for large datasets +- Index columns used in `WHERE`, `ORDER BY`, `JOIN` +- `withCount()` instead of loading relations to count +- `cursor()` for memory-efficient read-only iteration +- Never query in Blade templates + +### 2. Advanced Query Patterns → `rules/advanced-queries.md` + +- `addSelect()` subqueries over eager-loading entire has-many for a single value +- Dynamic relationships via subquery FK + `belongsTo` +- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries +- `setRelation()` to prevent circular N+1 queries +- `whereIn` + `pluck()` over `whereHas` for better index usage +- Two simple queries can beat one complex query +- Compound indexes matching `orderBy` column order +- Correlated subqueries in `orderBy` for has-many sorting (avoid joins) + +### 3. Security → `rules/security.md` + +- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates +- No raw SQL with user input — use Eloquent or query builder +- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes +- Validate MIME type, extension, and size for file uploads +- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields + +### 4. Caching → `rules/caching.md` + +- `Cache::remember()` over manual get/put +- `Cache::flexible()` for stale-while-revalidate on high-traffic data +- `Cache::memo()` to avoid redundant cache hits within a request +- Cache tags to invalidate related groups +- `Cache::add()` for atomic conditional writes +- `once()` to memoize per-request or per-object lifetime +- `Cache::lock()` / `lockForUpdate()` for race conditions +- Failover cache stores in production + +### 5. Eloquent Patterns → `rules/eloquent.md` + +- Correct relationship types with return type hints +- Local scopes for reusable query constraints +- Global scopes sparingly — document their existence +- Attribute casts in the `casts()` method +- Cast date columns, use Carbon instances in templates +- `whereBelongsTo($model)` for cleaner queries +- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries + +### 6. Validation & Forms → `rules/validation.md` + +- Form Request classes, not inline validation +- Array notation `['required', 'email']` for new code; follow existing convention +- `$request->validated()` only — never `$request->all()` +- `Rule::when()` for conditional validation +- `after()` instead of `withValidator()` + +### 7. Configuration → `rules/config.md` + +- `env()` only inside config files +- `App::environment()` or `app()->isProduction()` +- Config, lang files, and constants over hardcoded text + +### 8. Testing Patterns → `rules/testing.md` + +- `LazilyRefreshDatabase` over `RefreshDatabase` for speed +- `assertModelExists()` over raw `assertDatabaseHas()` +- Factory states and sequences over manual overrides +- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before +- `recycle()` to share relationship instances across factories + +### 9. Queue & Job Patterns → `rules/queue-jobs.md` + +- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]` +- `ShouldBeUnique` to prevent duplicates; `WithoutOverlapping::untilProcessing()` for concurrency +- Always implement `failed()`; with `retryUntil()`, set `$tries = 0` +- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs +- Horizon for complex multi-queue scenarios + +### 10. Routing & Controllers → `rules/routing.md` + +- Implicit route model binding +- Scoped bindings for nested resources +- `Route::resource()` or `apiResource()` +- Methods under 10 lines — extract to actions/services +- Type-hint Form Requests for auto-validation + +### 11. HTTP Client → `rules/http-client.md` + +- Explicit `timeout` and `connectTimeout` on every request +- `retry()` with exponential backoff for external APIs +- Check response status or use `throw()` +- `Http::pool()` for concurrent independent requests +- `Http::fake()` and `preventStrayRequests()` in tests + +### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md` + +- Event discovery over manual registration; `event:cache` in production +- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions +- Queue notifications and mailables with `ShouldQueue` +- On-demand notifications for non-user recipients +- `HasLocalePreference` on notifiable models +- `assertQueued()` not `assertSent()` for queued mailables +- Markdown mailables for transactional emails + +### 13. Error Handling → `rules/error-handling.md` + +- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern +- `ShouldntReport` for exceptions that should never log +- Throttle high-volume exceptions to protect log sinks +- `dontReportDuplicates()` for multi-catch scenarios +- Force JSON rendering for API routes +- Structured context via `context()` on exception classes + +### 14. Task Scheduling → `rules/scheduling.md` + +- `withoutOverlapping()` on variable-duration tasks +- `onOneServer()` on multi-server deployments +- `runInBackground()` for concurrent long tasks +- `environments()` to restrict to appropriate environments +- `takeUntilTimeout()` for time-bounded processing +- Schedule groups for shared configuration + +### 15. Architecture → `rules/architecture.md` + +- Single-purpose Action classes; dependency injection over `app()` helper +- Prefer official Laravel packages and follow conventions, don't override defaults +- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety +- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution + +### 16. Migrations → `rules/migrations.md` + +- Generate migrations with `php artisan make:migration` +- `constrained()` for foreign keys +- Never modify migrations that have run in production +- Add indexes in the migration, not as an afterthought +- Mirror column defaults in model `$attributes` +- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes +- One concern per migration — never mix DDL and DML + +### 17. Collections → `rules/collections.md` + +- Higher-order messages for simple collection operations +- `cursor()` vs. `lazy()` — choose based on relationship needs +- `lazyById()` when updating records while iterating +- `toQuery()` for bulk operations on collections + +### 18. Blade & Views → `rules/blade-views.md` + +- `$attributes->merge()` in component templates +- Blade components over `@include`; `@pushOnce` for per-component scripts +- View Composers for shared view data +- `@aware` for deeply nested component props + +### 19. Conventions & Style → `rules/style.md` + +- Follow Laravel naming conventions for all entities +- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions +- No JS/CSS in Blade, no HTML in PHP classes +- Code should be readable; comments only for config files + +## How to Apply + +Always use a sub-agent to read rule files and explore this skill's content. + +1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10) +2. Check sibling files for existing patterns — follow those first per Consistency First +3. Verify API syntax with `search-docs` for the installed Laravel version \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/advanced-queries.md b/.agents/skills/laravel-best-practices/rules/advanced-queries.md new file mode 100644 index 0000000..920714a --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/advanced-queries.md @@ -0,0 +1,106 @@ +# Advanced Query Patterns + +## Use `addSelect()` Subqueries for Single Values from Has-Many + +Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries. + +```php +public function scopeWithLastLoginAt($query): void +{ + $query->addSelect([ + 'last_login_at' => Login::select('created_at') + ->whereColumn('user_id', 'users.id') + ->latest() + ->take(1), + ])->withCasts(['last_login_at' => 'datetime']); +} +``` + +## Create Dynamic Relationships via Subquery FK + +Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection. + +```php +public function lastLogin(): BelongsTo +{ + return $this->belongsTo(Login::class); +} + +public function scopeWithLastLogin($query): void +{ + $query->addSelect([ + 'last_login_id' => Login::select('id') + ->whereColumn('user_id', 'users.id') + ->latest() + ->take(1), + ])->with('lastLogin'); +} +``` + +## Use Conditional Aggregates Instead of Multiple Count Queries + +Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values. + +```php +$statuses = Feature::toBase() + ->selectRaw("count(case when status = 'Requested' then 1 end) as requested") + ->selectRaw("count(case when status = 'Planned' then 1 end) as planned") + ->selectRaw("count(case when status = 'Completed' then 1 end) as completed") + ->first(); +``` + +## Use `setRelation()` to Prevent Circular N+1 + +When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries. + +```php +$feature->load('comments.user'); +$feature->comments->each->setRelation('feature', $feature); +``` + +## Prefer `whereIn` + Subquery Over `whereHas` + +`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory. + +Incorrect (correlated EXISTS re-executes per row): + +```php +$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term)); +``` + +Correct (index-friendly subquery, no PHP memory overhead): + +```php +$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id')); +``` + +## Sometimes Two Simple Queries Beat One Complex Query + +Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index. + +## Use Compound Indexes Matching `orderBy` Column Order + +When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index. + +```php +// Migration +$table->index(['last_name', 'first_name']); + +// Query — column order must match the index +User::query()->orderBy('last_name')->orderBy('first_name')->paginate(); +``` + +## Use Correlated Subqueries for Has-Many Ordering + +When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading. + +```php +public function scopeOrderByLastLogin($query): void +{ + $query->orderByDesc(Login::select('created_at') + ->whereColumn('user_id', 'users.id') + ->latest() + ->take(1) + ); +} +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/architecture.md b/.agents/skills/laravel-best-practices/rules/architecture.md new file mode 100644 index 0000000..1650564 --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/architecture.md @@ -0,0 +1,202 @@ +# Architecture Best Practices + +## Single-Purpose Action Classes + +Extract discrete business operations into invokable Action classes. + +```php +class CreateOrderAction +{ + public function __construct(private InventoryService $inventory) {} + + public function execute(array $data): Order + { + $order = Order::create($data); + $this->inventory->reserve($order); + + return $order; + } +} +``` + +## Use Dependency Injection + +Always use constructor injection. Avoid `app()` or `resolve()` inside classes. + +Incorrect: +```php +class OrderController extends Controller +{ + public function store(StoreOrderRequest $request) + { + $service = app(OrderService::class); + + return $service->create($request->validated()); + } +} +``` + +Correct: +```php +class OrderController extends Controller +{ + public function __construct(private OrderService $service) {} + + public function store(StoreOrderRequest $request) + { + return $this->service->create($request->validated()); + } +} +``` + +## Code to Interfaces + +Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability. + +Incorrect (concrete dependency): +```php +class OrderService +{ + public function __construct(private StripeGateway $gateway) {} +} +``` + +Correct (interface dependency): +```php +interface PaymentGateway +{ + public function charge(int $amount, string $customerId): PaymentResult; +} + +class OrderService +{ + public function __construct(private PaymentGateway $gateway) {} +} +``` + +Bind in a service provider: + +```php +$this->app->bind(PaymentGateway::class, StripeGateway::class); +``` + +## Default Sort by Descending + +When no explicit order is specified, sort by `id` or `created_at` descending. Explicit ordering prevents cross-database inconsistencies between MySQL and Postgres. + +Incorrect: +```php +$posts = Post::paginate(); +``` + +Correct: +```php +$posts = Post::latest()->paginate(); +``` + +## Use Atomic Locks for Race Conditions + +Prevent race conditions with `Cache::lock()` or `lockForUpdate()`. + +```php +Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) { + $order->process(); +}); + +// Or at query level +$product = Product::where('id', $id)->lockForUpdate()->first(); +``` + +## Use `mb_*` String Functions + +When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters. + +Incorrect: +```php +strlen('José'); // 5 (bytes, not characters) +strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte +``` + +Correct: +```php +mb_strlen('José'); // 4 (characters) +mb_strtolower('MÜNCHEN'); // 'münchen' + +// Prefer Laravel's Str helpers when available +Str::length('José'); // 4 +Str::lower('MÜNCHEN'); // 'münchen' +``` + +## Use `defer()` for Post-Response Work + +For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead. + +Incorrect (job overhead for trivial work): +```php +dispatch(new LogPageView($page)); +``` + +Correct (runs after response, same process): +```php +defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()])); +``` + +Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work. + +## Use `Context` for Request-Scoped Data + +The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually. + +```php +// In middleware +Context::add('tenant_id', $request->header('X-Tenant-ID')); + +// Anywhere later — controllers, jobs, log context +$tenantId = Context::get('tenant_id'); +``` + +Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`. + +## Use `Concurrency::run()` for Parallel Execution + +Run independent operations in parallel using child processes — no async libraries needed. + +```php +use Illuminate\Support\Facades\Concurrency; + +[$users, $orders] = Concurrency::run([ + fn () => User::count(), + fn () => Order::where('status', 'pending')->count(), +]); +``` + +Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially. + +## Convention Over Configuration + +Follow Laravel conventions. Don't override defaults unnecessarily. + +Incorrect: +```php +class Customer extends Model +{ + protected $table = 'Customer'; + protected $primaryKey = 'customer_id'; + + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id'); + } +} +``` + +Correct: +```php +class Customer extends Model +{ + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class); + } +} +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/blade-views.md b/.agents/skills/laravel-best-practices/rules/blade-views.md new file mode 100644 index 0000000..c6f8aaf --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/blade-views.md @@ -0,0 +1,36 @@ +# Blade & Views Best Practices + +## Use `$attributes->merge()` in Component Templates + +Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly. + +```blade +
merge(['class' => 'alert alert-'.$type]) }}> + {{ $message }} +
+``` + +## Use `@pushOnce` for Per-Component Scripts + +If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once. + +## Prefer Blade Components Over `@include` + +`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots. + +## Use View Composers for Shared View Data + +If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it. + +## Use Blade Fragments for Partial Re-Renders (htmx/Turbo) + +A single view can return either the full page or just a fragment, keeping routing clean. + +```php +return view('dashboard', compact('users')) + ->fragmentIf($request->hasHeader('HX-Request'), 'user-list'); +``` + +## Use `@aware` for Deeply Nested Component Props + +Avoids re-passing parent props through every level of nested components. \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/caching.md b/.agents/skills/laravel-best-practices/rules/caching.md new file mode 100644 index 0000000..eb3ef3e --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/caching.md @@ -0,0 +1,70 @@ +# Caching Best Practices + +## Use `Cache::remember()` Instead of Manual Get/Put + +Atomic pattern prevents race conditions and removes boilerplate. + +Incorrect: +```php +$val = Cache::get('stats'); +if (! $val) { + $val = $this->computeStats(); + Cache::put('stats', $val, 60); +} +``` + +Correct: +```php +$val = Cache::remember('stats', 60, fn () => $this->computeStats()); +``` + +## Use `Cache::flexible()` for Stale-While-Revalidate + +On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background. + +Incorrect: `Cache::remember('users', 300, fn () => User::all());` + +Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function. + +## Use `Cache::memo()` to Avoid Redundant Hits Within a Request + +If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory. + +`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5. + +## Use Cache Tags to Invalidate Related Groups + +Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`. + +```php +Cache::tags(['user-1'])->flush(); +``` + +## Use `Cache::add()` for Atomic Conditional Writes + +`add()` only writes if the key does not exist — atomic, no race condition between checking and writing. + +Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }` + +Correct: `Cache::add('lock', true, 10);` + +## Use `once()` for Per-Request Memoization + +`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory. + +```php +public function roles(): Collection +{ + return once(fn () => $this->loadRoles()); +} +``` + +Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching. + +## Configure Failover Cache Stores in Production + +If Redis goes down, the app falls back to a secondary store automatically. + +```php +'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']], +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/collections.md b/.agents/skills/laravel-best-practices/rules/collections.md new file mode 100644 index 0000000..14f683d --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/collections.md @@ -0,0 +1,44 @@ +# Collection Best Practices + +## Use Higher-Order Messages for Simple Operations + +Incorrect: +```php +$users->each(function (User $user) { + $user->markAsVip(); +}); +``` + +Correct: `$users->each->markAsVip();` + +Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc. + +## Choose `cursor()` vs. `lazy()` Correctly + +- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk). +- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading. + +Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored. + +Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work. + +## Use `lazyById()` When Updating Records While Iterating + +`lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation. + +## Use `toQuery()` for Bulk Operations on Collections + +Avoids manual `whereIn` construction. + +Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);` + +Correct: `$users->toQuery()->update([...]);` + +## Use `#[CollectedBy]` for Custom Collection Classes + +More declarative than overriding `newCollection()`. + +```php +#[CollectedBy(UserCollection::class)] +class User extends Model {} +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/config.md b/.agents/skills/laravel-best-practices/rules/config.md new file mode 100644 index 0000000..8fd8f53 --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/config.md @@ -0,0 +1,73 @@ +# Configuration Best Practices + +## `env()` Only in Config Files + +Direct `env()` calls return `null` when config is cached. + +Incorrect: +```php +$key = env('API_KEY'); +``` + +Correct: +```php +// config/services.php +'key' => env('API_KEY'), + +// Application code +$key = config('services.key'); +``` + +## Use Encrypted Env or External Secrets + +Never store production secrets in plain `.env` files in version control. + +Incorrect: +```bash + +# .env committed to repo or shared in Slack + +STRIPE_SECRET=sk_live_abc123 +AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI +``` + +Correct: +```bash +php artisan env:encrypt --env=production --readable +php artisan env:decrypt --env=production +``` + +For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime. + +## Use `App::environment()` for Environment Checks + +Incorrect: +```php +if (env('APP_ENV') === 'production') { +``` + +Correct: +```php +if (app()->isProduction()) { +// or +if (App::environment('production')) { +``` + +## Use Constants and Language Files + +Use class constants instead of hardcoded magic strings for model states, types, and statuses. + +```php +// Incorrect +return $this->type === 'normal'; + +// Correct +return $this->type === self::TYPE_NORMAL; +``` + +If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there. + +```php +// Only when lang files already exist in the project +return back()->with('message', __('app.article_added')); +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/db-performance.md b/.agents/skills/laravel-best-practices/rules/db-performance.md new file mode 100644 index 0000000..8fb7193 --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/db-performance.md @@ -0,0 +1,192 @@ +# Database Performance Best Practices + +## Always Eager Load Relationships + +Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront. + +Incorrect (N+1 — executes 1 + N queries): +```php +$posts = Post::all(); +foreach ($posts as $post) { + echo $post->author->name; +} +``` + +Correct (2 queries total): +```php +$posts = Post::with('author')->get(); +foreach ($posts as $post) { + echo $post->author->name; +} +``` + +Constrain eager loads to select only needed columns (always include the foreign key): + +```php +$users = User::with(['posts' => function ($query) { + $query->select('id', 'user_id', 'title') + ->where('published', true) + ->latest() + ->limit(10); +}])->get(); +``` + +## Prevent Lazy Loading in Development + +Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development. + +```php +public function boot(): void +{ + Model::preventLazyLoading(! app()->isProduction()); +} +``` + +Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded. + +## Select Only Needed Columns + +Avoid `SELECT *` — especially when tables have large text or JSON columns. + +Incorrect: +```php +$posts = Post::with('author')->get(); +``` + +Correct: +```php +$posts = Post::select('id', 'title', 'user_id', 'created_at') + ->with(['author:id,name,avatar']) + ->get(); +``` + +When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match. + +## Chunk Large Datasets + +Never load thousands of records at once. Use chunking for batch processing. + +Incorrect: +```php +$users = User::all(); +foreach ($users as $user) { + $user->notify(new WeeklyDigest); +} +``` + +Correct: +```php +User::where('subscribed', true)->chunk(200, function ($users) { + foreach ($users as $user) { + $user->notify(new WeeklyDigest); + } +}); +``` + +Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change: + +```php +User::where('active', false)->chunkById(200, function ($users) { + $users->each->delete(); +}); +``` + +## Add Database Indexes + +Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses. + +Incorrect: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained(); + $table->string('status'); + $table->timestamps(); +}); +``` + +Correct: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->index()->constrained(); + $table->string('status')->index(); + $table->timestamps(); + $table->index(['status', 'created_at']); +}); +``` + +Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`). + +## Use `withCount()` for Counting Relations + +Never load entire collections just to count them. + +Incorrect: +```php +$posts = Post::all(); +foreach ($posts as $post) { + echo $post->comments->count(); +} +``` + +Correct: +```php +$posts = Post::withCount('comments')->get(); +foreach ($posts as $post) { + echo $post->comments_count; +} +``` + +Conditional counting: + +```php +$posts = Post::withCount([ + 'comments', + 'comments as approved_comments_count' => function ($query) { + $query->where('approved', true); + }, +])->get(); +``` + +## Use `cursor()` for Memory-Efficient Iteration + +For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator. + +Incorrect: +```php +$users = User::where('active', true)->get(); +``` + +Correct: +```php +foreach (User::where('active', true)->cursor() as $user) { + ProcessUser::dispatch($user->id); +} +``` + +Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records. + +## No Queries in Blade Templates + +Never execute queries in Blade templates. Pass data from controllers. + +Incorrect: +```blade +@foreach (User::all() as $user) + {{ $user->profile->name }} +@endforeach +``` + +Correct: +```php +// Controller +$users = User::with('profile')->get(); +return view('users.index', compact('users')); +``` + +```blade +@foreach ($users as $user) + {{ $user->profile->name }} +@endforeach +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/eloquent.md b/.agents/skills/laravel-best-practices/rules/eloquent.md new file mode 100644 index 0000000..09cd66a --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/eloquent.md @@ -0,0 +1,148 @@ +# Eloquent Best Practices + +## Use Correct Relationship Types + +Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints. + +```php +public function comments(): HasMany +{ + return $this->hasMany(Comment::class); +} + +public function author(): BelongsTo +{ + return $this->belongsTo(User::class, 'user_id'); +} +``` + +## Use Local Scopes for Reusable Queries + +Extract reusable query constraints into local scopes to avoid duplication. + +Incorrect: +```php +$active = User::where('verified', true)->whereNotNull('activated_at')->get(); +$articles = Article::whereHas('user', function ($q) { + $q->where('verified', true)->whereNotNull('activated_at'); +})->get(); +``` + +Correct: +```php +public function scopeActive(Builder $query): Builder +{ + return $query->where('verified', true)->whereNotNull('activated_at'); +} + +// Usage +$active = User::active()->get(); +$articles = Article::whereHas('user', fn ($q) => $q->active())->get(); +``` + +## Apply Global Scopes Sparingly + +Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy. + +Incorrect (global scope for a conditional filter): +```php +class PublishedScope implements Scope +{ + public function apply(Builder $builder, Model $model): void + { + $builder->where('published', true); + } +} +// Now admin panels, reports, and background jobs all silently skip drafts +``` + +Correct (local scope you opt into): +```php +public function scopePublished(Builder $query): Builder +{ + return $query->where('published', true); +} + +Post::published()->paginate(); // Explicit +Post::paginate(); // Admin sees all +``` + +## Define Attribute Casts + +Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion. + +```php +protected function casts(): array +{ + return [ + 'is_active' => 'boolean', + 'metadata' => 'array', + 'total' => 'decimal:2', + ]; +} +``` + +## Cast Date Columns Properly + +Always cast date columns. Use Carbon instances in templates instead of formatting strings manually. + +Incorrect: +```blade +{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }} +``` + +Correct: +```php +protected function casts(): array +{ + return [ + 'ordered_at' => 'datetime', + ]; +} +``` + +```blade +{{ $order->ordered_at->toDateString() }} +{{ $order->ordered_at->format('m-d') }} +``` + +## Use `whereBelongsTo()` for Relationship Queries + +Cleaner than manually specifying foreign keys. + +Incorrect: +```php +Post::where('user_id', $user->id)->get(); +``` + +Correct: +```php +Post::whereBelongsTo($user)->get(); +Post::whereBelongsTo($user, 'author')->get(); +``` + +## Avoid Hardcoded Table Names in Queries + +Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string). + +Incorrect: +```php +DB::table('users')->where('active', true)->get(); + +$query->join('companies', 'companies.id', '=', 'users.company_id'); + +DB::select('SELECT * FROM orders WHERE status = ?', ['pending']); +``` + +Correct — reference the model's table: +```php +DB::table((new User)->getTable())->where('active', true)->get(); + +// Even better — use Eloquent or the query builder instead of raw SQL +User::where('active', true)->get(); +Order::where('status', 'pending')->get(); +``` + +Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable. + +**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration. \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/error-handling.md b/.agents/skills/laravel-best-practices/rules/error-handling.md new file mode 100644 index 0000000..bb8e7a3 --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/error-handling.md @@ -0,0 +1,72 @@ +# Error Handling Best Practices + +## Exception Reporting and Rendering + +There are two valid approaches — choose one and apply it consistently across the project. + +**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find: + +```php +class InvalidOrderException extends Exception +{ + public function report(): void { /* custom reporting */ } + + public function render(Request $request): Response + { + return response()->view('errors.invalid-order', status: 422); + } +} +``` + +**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture: + +```php +->withExceptions(function (Exceptions $exceptions) { + $exceptions->report(function (InvalidOrderException $e) { /* ... */ }); + $exceptions->render(function (InvalidOrderException $e, Request $request) { + return response()->view('errors.invalid-order', status: 422); + }); +}) +``` + +Check the existing codebase and follow whichever pattern is already established. + +## Use `ShouldntReport` for Exceptions That Should Never Log + +More discoverable than listing classes in `dontReport()`. + +```php +class PodcastProcessingException extends Exception implements ShouldntReport {} +``` + +## Throttle High-Volume Exceptions + +A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type. + +## Enable `dontReportDuplicates()` + +Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks. + +## Force JSON Error Rendering for API Routes + +Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes. + +```php +$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { + return $request->is('api/*') || $request->expectsJson(); +}); +``` + +## Add Context to Exception Classes + +Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry. + +```php +class InvalidOrderException extends Exception +{ + public function context(): array + { + return ['order_id' => $this->orderId]; + } +} +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/events-notifications.md b/.agents/skills/laravel-best-practices/rules/events-notifications.md new file mode 100644 index 0000000..bc43f19 --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/events-notifications.md @@ -0,0 +1,48 @@ +# Events & Notifications Best Practices + +## Rely on Event Discovery + +Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`. + +## Run `event:cache` in Production Deploy + +Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`. + +## Use `ShouldDispatchAfterCommit` Inside Transactions + +Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet. + +```php +class OrderShipped implements ShouldDispatchAfterCommit {} +``` + +## Always Queue Notifications + +Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response. + +```php +class InvoicePaid extends Notification implements ShouldQueue +{ + use Queueable; +} +``` + +## Use `afterCommit()` on Notifications in Transactions + +Same race condition as events — the queued notification job may run before the transaction commits. + +## Route Notification Channels to Dedicated Queues + +Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues. + +## Use On-Demand Notifications for Non-User Recipients + +Avoid creating dummy models to send notifications to arbitrary addresses. + +```php +Notification::route('mail', 'admin@example.com')->notify(new SystemAlert()); +``` + +## Implement `HasLocalePreference` on Notifiable Models + +Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed. \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/http-client.md b/.agents/skills/laravel-best-practices/rules/http-client.md new file mode 100644 index 0000000..0a7876e --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/http-client.md @@ -0,0 +1,160 @@ +# HTTP Client Best Practices + +## Always Set Explicit Timeouts + +The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast. + +Incorrect: +```php +$response = Http::get('https://api.example.com/users'); +``` + +Correct: +```php +$response = Http::timeout(5) + ->connectTimeout(3) + ->get('https://api.example.com/users'); +``` + +For service-specific clients, define timeouts in a macro: + +```php +Http::macro('github', function () { + return Http::baseUrl('https://api.github.com') + ->timeout(10) + ->connectTimeout(3) + ->withToken(config('services.github.token')); +}); + +$response = Http::github()->get('/repos/laravel/framework'); +``` + +## Use Retry with Backoff for External APIs + +External APIs have transient failures. Use `retry()` with increasing delays. + +Incorrect: +```php +$response = Http::post('https://api.stripe.com/v1/charges', $data); + +if ($response->failed()) { + throw new PaymentFailedException('Charge failed'); +} +``` + +Correct: +```php +$response = Http::retry([100, 500, 1000]) + ->timeout(10) + ->post('https://api.stripe.com/v1/charges', $data); +``` + +Only retry on specific errors: + +```php +$response = Http::retry(3, 100, function (Exception $exception, PendingRequest $request) { + return $exception instanceof ConnectionException + || ($exception instanceof RequestException && $exception->response->serverError()); +})->post('https://api.example.com/data'); +``` + +## Handle Errors Explicitly + +The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`. + +Incorrect: +```php +$response = Http::get('https://api.example.com/users/1'); +$user = $response->json(); // Could be an error body +``` + +Correct: +```php +$response = Http::timeout(5) + ->get('https://api.example.com/users/1') + ->throw(); + +$user = $response->json(); +``` + +For graceful degradation: + +```php +$response = Http::get('https://api.example.com/users/1'); + +if ($response->successful()) { + return $response->json(); +} + +if ($response->notFound()) { + return null; +} + +$response->throw(); +``` + +## Use Request Pooling for Concurrent Requests + +When making multiple independent API calls, use `Http::pool()` instead of sequential calls. + +Incorrect: +```php +$users = Http::get('https://api.example.com/users')->json(); +$posts = Http::get('https://api.example.com/posts')->json(); +$comments = Http::get('https://api.example.com/comments')->json(); +``` + +Correct: +```php +use Illuminate\Http\Client\Pool; + +$responses = Http::pool(fn (Pool $pool) => [ + $pool->as('users')->get('https://api.example.com/users'), + $pool->as('posts')->get('https://api.example.com/posts'), + $pool->as('comments')->get('https://api.example.com/comments'), +]); + +$users = $responses['users']->json(); +$posts = $responses['posts']->json(); +``` + +## Fake HTTP Calls in Tests + +Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`. + +Incorrect: +```php +it('syncs user from API', function () { + $service = new UserSyncService; + $service->sync(1); // Hits the real API +}); +``` + +Correct: +```php +it('syncs user from API', function () { + Http::preventStrayRequests(); + + Http::fake([ + 'api.example.com/users/1' => Http::response([ + 'name' => 'John Doe', + 'email' => 'john@example.com', + ]), + ]); + + $service = new UserSyncService; + $service->sync(1); + + Http::assertSent(function (Request $request) { + return $request->url() === 'https://api.example.com/users/1'; + }); +}); +``` + +Test failure scenarios too: + +```php +Http::fake([ + 'api.example.com/*' => Http::failedConnection(), +]); +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/mail.md b/.agents/skills/laravel-best-practices/rules/mail.md new file mode 100644 index 0000000..c7f6796 --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/mail.md @@ -0,0 +1,27 @@ +# Mail Best Practices + +## Implement `ShouldQueue` on the Mailable Class + +Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it. + +## Use `afterCommit()` on Mailables Inside Transactions + +A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor. + +## Use `assertQueued()` Not `assertSent()` for Queued Mailables + +`Mail::assertSent()` only catches synchronous mail. Queued mailables silently pass `assertSent`, giving false confidence. + +Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`. + +Correct: `Mail::assertQueued(OrderShipped::class);` + +## Use Markdown Mailables for Transactional Emails + +Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag. + +## Separate Content Tests from Sending Tests + +Content tests: instantiate the mailable directly, call `assertSeeInHtml()`. +Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`. +Don't mix them — it conflates concerns and makes tests brittle. \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/migrations.md b/.agents/skills/laravel-best-practices/rules/migrations.md new file mode 100644 index 0000000..de25aa3 --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/migrations.md @@ -0,0 +1,121 @@ +# Migration Best Practices + +## Generate Migrations with Artisan + +Always use `php artisan make:migration` for consistent naming and timestamps. + +Incorrect (manually created file): +```php +// database/migrations/posts_migration.php ← wrong naming, no timestamp +``` + +Correct (Artisan-generated): +```bash +php artisan make:migration create_posts_table +php artisan make:migration add_slug_to_posts_table +``` + +## Use `constrained()` for Foreign Keys + +Automatic naming and referential integrity. + +```php +$table->foreignId('user_id')->constrained()->cascadeOnDelete(); + +// Non-standard names +$table->foreignId('author_id')->constrained('users'); +``` + +## Never Modify Deployed Migrations + +Once a migration has run in production, treat it as immutable. Create a new migration to change the table. + +Incorrect (editing a deployed migration): +```php +// 2024_01_01_create_posts_table.php — already in production +$table->string('slug')->unique(); // ← added after deployment +``` + +Correct (new migration to alter): +```php +// 2024_03_15_add_slug_to_posts_table.php +Schema::table('posts', function (Blueprint $table) { + $table->string('slug')->unique()->after('title'); +}); +``` + +## Add Indexes in the Migration + +Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes. + +Incorrect: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained(); + $table->string('status'); + $table->timestamps(); +}); +``` + +Correct: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained()->index(); + $table->string('status')->index(); + $table->timestamp('shipped_at')->nullable()->index(); + $table->timestamps(); +}); +``` + +## Mirror Defaults in Model `$attributes` + +When a column has a database default, mirror it in the model so new instances have correct values before saving. + +```php +// Migration +$table->string('status')->default('pending'); + +// Model +protected $attributes = [ + 'status' => 'pending', +]; +``` + +## Write Reversible `down()` Methods by Default + +Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments. + +```php +public function down(): void +{ + Schema::table('posts', function (Blueprint $table) { + $table->dropColumn('slug'); + }); +} +``` + +For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported. + +## Keep Migrations Focused + +One concern per migration. Never mix DDL (schema changes) and DML (data manipulation). + +Incorrect (partial failure creates unrecoverable state): +```php +public function up(): void +{ + Schema::create('settings', function (Blueprint $table) { ... }); + DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']); +} +``` + +Correct (separate migrations): +```php +// Migration 1: create_settings_table +Schema::create('settings', function (Blueprint $table) { ... }); + +// Migration 2: seed_default_settings +DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']); +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/queue-jobs.md b/.agents/skills/laravel-best-practices/rules/queue-jobs.md new file mode 100644 index 0000000..d4575aa --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/queue-jobs.md @@ -0,0 +1,146 @@ +# Queue & Job Best Practices + +## Set `retry_after` Greater Than `timeout` + +If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution. + +Incorrect (`retry_after` ≤ `timeout`): +```php +class ProcessReport implements ShouldQueue +{ + public $timeout = 120; +} + +// config/queue.php — retry_after: 90 ← job retried while still running! +``` + +Correct (`retry_after` > `timeout`): +```php +class ProcessReport implements ShouldQueue +{ + public $timeout = 120; +} + +// config/queue.php — retry_after: 180 ← safely longer than any job timeout +``` + +## Use Exponential Backoff + +Use progressively longer delays between retries to avoid hammering failing services. + +Incorrect (fixed retry interval): +```php +class SyncWithStripe implements ShouldQueue +{ + public $tries = 3; + // Default: retries immediately, overwhelming the API +} +``` + +Correct (exponential backoff): +```php +class SyncWithStripe implements ShouldQueue +{ + public $tries = 3; + public $backoff = [1, 5, 10]; +} +``` + +## Implement `ShouldBeUnique` + +Prevent duplicate job processing. + +```php +class GenerateInvoice implements ShouldQueue, ShouldBeUnique +{ + public function uniqueId(): string + { + return $this->order->id; + } + + public $uniqueFor = 3600; +} +``` + +## Always Implement `failed()` + +Handle errors explicitly — don't rely on silent failure. + +```php +public function failed(?Throwable $exception): void +{ + $this->podcast->update(['status' => 'failed']); + Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]); +} +``` + +## Rate Limit External API Calls in Jobs + +Use `RateLimited` middleware to throttle jobs calling third-party APIs. + +```php +public function middleware(): array +{ + return [new RateLimited('external-api')]; +} +``` + +## Batch Related Jobs + +Use `Bus::batch()` when jobs should succeed or fail together. + +```php +Bus::batch([ + new ImportCsvChunk($chunk1), + new ImportCsvChunk($chunk2), +]) +->then(fn (Batch $batch) => Notification::send($user, new ImportComplete)) +->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed')) +->dispatch(); +``` + +## `retryUntil()` Needs `$tries = 0` + +When using time-based retry limits, set `$tries = 0` to avoid premature failure. + +```php +public $tries = 0; + +public function retryUntil(): DateTime +{ + return now()->addHours(4); +} +``` + +## Use `WithoutOverlapping::untilProcessing()` + +Prevents concurrent execution while allowing new instances to queue. + +```php +public function middleware(): array +{ + return [new WithoutOverlapping($this->product->id)->untilProcessing()]; +} +``` + +Without `untilProcessing()`, the lock extends through queue wait time. With it, the lock releases when processing starts. + +## Use Horizon for Complex Queue Scenarios + +Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities. + +```php +// config/horizon.php +'environments' => [ + 'production' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['high', 'default', 'low'], + 'balance' => 'auto', + 'minProcesses' => 1, + 'maxProcesses' => 10, + 'tries' => 3, + ], + ], +], +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/routing.md b/.agents/skills/laravel-best-practices/rules/routing.md new file mode 100644 index 0000000..e288375 --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/routing.md @@ -0,0 +1,98 @@ +# Routing & Controllers Best Practices + +## Use Implicit Route Model Binding + +Let Laravel resolve models automatically from route parameters. + +Incorrect: +```php +public function show(int $id) +{ + $post = Post::findOrFail($id); +} +``` + +Correct: +```php +public function show(Post $post) +{ + return view('posts.show', ['post' => $post]); +} +``` + +## Use Scoped Bindings for Nested Resources + +Enforce parent-child relationships automatically. + +```php +Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) { + // $post is automatically scoped to $user +})->scopeBindings(); +``` + +## Use Resource Controllers + +Use `Route::resource()` or `apiResource()` for RESTful endpoints. + +```php +Route::resource('posts', PostController::class); +Route::apiResource('api/posts', Api\PostController::class); +``` + +## Keep Controllers Thin + +Aim for under 10 lines per method. Extract business logic to action or service classes. + +Incorrect: +```php +public function store(Request $request) +{ + $validated = $request->validate([...]); + if ($request->hasFile('image')) { + $request->file('image')->move(public_path('images')); + } + $post = Post::create($validated); + $post->tags()->sync($validated['tags']); + event(new PostCreated($post)); + return redirect()->route('posts.show', $post); +} +``` + +Correct: +```php +public function store(StorePostRequest $request, CreatePostAction $create) +{ + $post = $create->execute($request->validated()); + + return redirect()->route('posts.show', $post); +} +``` + +## Type-Hint Form Requests + +Type-hinting Form Requests triggers automatic validation and authorization before the method executes. + +Incorrect: +```php +public function store(Request $request): RedirectResponse +{ + $validated = $request->validate([ + 'title' => ['required', 'max:255'], + 'body' => ['required'], + ]); + + Post::create($validated); + + return redirect()->route('posts.index'); +} +``` + +Correct: +```php +public function store(StorePostRequest $request): RedirectResponse +{ + Post::create($request->validated()); + + return redirect()->route('posts.index'); +} +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/scheduling.md b/.agents/skills/laravel-best-practices/rules/scheduling.md new file mode 100644 index 0000000..dfaefa2 --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/scheduling.md @@ -0,0 +1,39 @@ +# Task Scheduling Best Practices + +## Use `withoutOverlapping()` on Variable-Duration Tasks + +Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion. + +## Use `onOneServer()` on Multi-Server Deployments + +Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached). + +## Use `runInBackground()` for Concurrent Long Tasks + +By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes. + +## Use `environments()` to Restrict Tasks + +Prevent accidental execution of production-only tasks (billing, reporting) on staging. + +```php +Schedule::command('billing:charge')->monthly()->environments(['production']); +``` + +## Use `takeUntilTimeout()` for Time-Bounded Processing + +A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time. + +## Use Schedule Groups for Shared Configuration + +Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks. + +```php +Schedule::daily() + ->onOneServer() + ->timezone('America/New_York') + ->group(function () { + Schedule::command('emails:send --force'); + Schedule::command('emails:prune'); + }); +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/security.md b/.agents/skills/laravel-best-practices/rules/security.md new file mode 100644 index 0000000..524d47e --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/security.md @@ -0,0 +1,198 @@ +# Security Best Practices + +## Mass Assignment Protection + +Every model must define `$fillable` (whitelist) or `$guarded` (blacklist). + +Incorrect: +```php +class User extends Model +{ + protected $guarded = []; // All fields are mass assignable +} +``` + +Correct: +```php +class User extends Model +{ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; +} +``` + +Never use `$guarded = []` on models that accept user input. + +## Authorize Every Action + +Use policies or gates in controllers. Never skip authorization. + +Incorrect: +```php +public function update(Request $request, Post $post) +{ + $post->update($request->validated()); +} +``` + +Correct: +```php +public function update(UpdatePostRequest $request, Post $post) +{ + Gate::authorize('update', $post); + + $post->update($request->validated()); +} +``` + +Or via Form Request: + +```php +public function authorize(): bool +{ + return $this->user()->can('update', $this->route('post')); +} +``` + +## Prevent SQL Injection + +Always use parameter binding. Never interpolate user input into queries. + +Incorrect: +```php +DB::select("SELECT * FROM users WHERE name = '{$request->name}'"); +``` + +Correct: +```php +User::where('name', $request->name)->get(); + +// Raw expressions with bindings +User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get(); +``` + +## Escape Output to Prevent XSS + +Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content. + +Incorrect: +```blade +{!! $user->bio !!} +``` + +Correct: +```blade +{{ $user->bio }} +``` + +## CSRF Protection + +Include `@csrf` in all POST/PUT/DELETE Blade forms. Not needed in Inertia. + +Incorrect: +```blade +
+ +
+``` + +Correct: +```blade +
+ @csrf + +
+``` + +## Rate Limit Auth and API Routes + +Apply `throttle` middleware to authentication and API routes. + +```php +RateLimiter::for('login', function (Request $request) { + return Limit::perMinute(5)->by($request->ip()); +}); + +Route::post('/login', LoginController::class)->middleware('throttle:login'); +``` + +## Validate File Uploads + +Validate MIME type, extension, and size. Never trust client-provided filenames. + +```php +public function rules(): array +{ + return [ + 'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'], + ]; +} +``` + +Store with generated filenames: + +```php +$path = $request->file('avatar')->store('avatars', 'public'); +``` + +## Keep Secrets Out of Code + +Never commit `.env`. Access secrets via `config()` only. + +Incorrect: +```php +$key = env('API_KEY'); +``` + +Correct: +```php +// config/services.php +'api_key' => env('API_KEY'), + +// In application code +$key = config('services.api_key'); +``` + +## Audit Dependencies + +Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment. + +```bash +composer audit +``` + +## Encrypt Sensitive Database Fields + +Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`. + +Incorrect: +```php +class Integration extends Model +{ + protected function casts(): array + { + return [ + 'api_key' => 'string', + ]; + } +} +``` + +Correct: +```php +class Integration extends Model +{ + protected $hidden = ['api_key', 'api_secret']; + + protected function casts(): array + { + return [ + 'api_key' => 'encrypted', + 'api_secret' => 'encrypted', + ]; + } +} +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/style.md b/.agents/skills/laravel-best-practices/rules/style.md new file mode 100644 index 0000000000000000000000000000000000000000..db689bf774d1763ac3ec520a3874015f5421ff5b GIT binary patch literal 4443 zcmb7H|8g6*5$@mj6gxJXBU_TPGV!!SM{H%Ls*_r-YD&%@j)w&AKoK^0I0HCRY@C@s zM4zxv(rqc3Rg4_YS46<&Z-l2$9V!-oHYUfv=K_C|PowZt|LcB75;$1eTUe78Vh&a+E%-f!B^_a3OyuOh!1j+3CF~xh&C6Q z*=`XQmT8HzBMo|P)XsSFwYJu8q05a}Nq8>Un^s}fxWXTc+D!ClW^}bJz?`D4gwP={HL5A^SnD#A?)(C5LRZ zR@zG|^YKcHT#n048H9Q7>X){{QHr&?hq_KiVE^8jd(B0!WswWps*3d4DH&@1R8(75 z(o_>v@X2ovWz1l+2v>~J<^>HjOLin4++uISp>Q7sc= zww6}<$`&|bt}L=XnXE+ip&z}g_gV`3HWN5EPEweC&DDJI?qyj{CR+efKb>jeTy0sD zWtYI5qv?KwV!+7*dZa^2FYxC)TK@TN*ocD0=F&btK-5a%Wxf!e#ktaJd!wnwhV#M0 z|0*OpGDbs1S7xm&uSe55UhMTw=n7u9VGYd&_0x8mxwqVDzMxBM#erT(T>>Yi70@@Er6gkS%swHiLEM*)?2zc&R!b$)u{^0DPCWn-5getf^ zqwL-7)#&%+#9FdML00VP=EV)It0I7c8`GuUi-V&wR=MBE?KnxIkV45w0(RotSq(2 z5JBRcjqs-zn!;f43?h8rSf*Nmx8L*f!4K&P%HqkB0gWjgkH;zaLPU;y;I-Mt_EVJK z5225`U*USQfgjQV7uBs9|qK z8N(d$vlHhUP>F9>t{hc! zpk(1NTthYH##aU%kYRXf1Z5TJ#a|1EE=6q<>`nN5$zBCL1$xV1li$&!Wm{EWjJgF+mOY z2m}FlAue$xtpY2C90Ue9gpB9NU51F{eEN|$BM6k3O;JN_tnZ3KW*Cu$J)pVA7jKfx zaAd+LQR$pk$3cm3G=+DZ*%xEtGUz;w>gE-uon7-1V<>b8zw_u7Tofqy@TeZsE$W5A zjUuG+Q%gDQht~?1jCf*ZqtdaFudezoDD5U(J5b>JS*l%5P?3c6WmL#(LhH_DBsfbQ>Dd4CXi F{|%Ge#T5Vm literal 0 HcmV?d00001 diff --git a/.agents/skills/laravel-best-practices/rules/testing.md b/.agents/skills/laravel-best-practices/rules/testing.md new file mode 100644 index 0000000..d39cc3e --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/testing.md @@ -0,0 +1,43 @@ +# Testing Best Practices + +## Use `LazilyRefreshDatabase` Over `RefreshDatabase` + +`RefreshDatabase` runs all migrations every test run even when the schema hasn't changed. `LazilyRefreshDatabase` only migrates when needed, significantly speeding up large suites. + +## Use Model Assertions Over Raw Database Assertions + +Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);` + +Correct: `$this->assertModelExists($user);` + +More expressive, type-safe, and fails with clearer messages. + +## Use Factory States and Sequences + +Named states make tests self-documenting. Sequences eliminate repetitive setup. + +Incorrect: `User::factory()->create(['email_verified_at' => null]);` + +Correct: `User::factory()->unverified()->create();` + +## Use `Exceptions::fake()` to Assert Exception Reporting + +Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally. + +## Call `Event::fake()` After Factory Setup + +Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models. + +Incorrect: `Event::fake(); $user = User::factory()->create();` + +Correct: `$user = User::factory()->create(); Event::fake();` + +## Use `recycle()` to Share Relationship Instances Across Factories + +Without `recycle()`, nested factories create separate instances of the same conceptual entity. + +```php +Ticket::factory() + ->recycle(Airline::factory()->create()) + ->create(); +``` \ No newline at end of file diff --git a/.agents/skills/laravel-best-practices/rules/validation.md b/.agents/skills/laravel-best-practices/rules/validation.md new file mode 100644 index 0000000..a20202f --- /dev/null +++ b/.agents/skills/laravel-best-practices/rules/validation.md @@ -0,0 +1,75 @@ +# Validation & Forms Best Practices + +## Use Form Request Classes + +Extract validation from controllers into dedicated Form Request classes. + +Incorrect: +```php +public function store(Request $request) +{ + $request->validate([ + 'title' => 'required|max:255', + 'body' => 'required', + ]); +} +``` + +Correct: +```php +public function store(StorePostRequest $request) +{ + Post::create($request->validated()); +} +``` + +## Array vs. String Notation for Rules + +Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses. + +```php +// Preferred for new code +'email' => ['required', 'email', Rule::unique('users')], + +// Follow existing convention if the project uses string notation +'email' => 'required|email|unique:users', +``` + +## Always Use `validated()` + +Get only validated data. Never use `$request->all()` for mass operations. + +Incorrect: +```php +Post::create($request->all()); +``` + +Correct: +```php +Post::create($request->validated()); +``` + +## Use `Rule::when()` for Conditional Validation + +```php +'company_name' => [ + Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']), +], +``` + +## Use the `after()` Method for Custom Validation + +Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields. + +```php +public function after(): array +{ + return [ + function (Validator $validator) { + if ($this->quantity > Product::find($this->product_id)?->stock) { + $validator->errors()->add('quantity', 'Not enough stock.'); + } + }, + ]; +} +``` \ No newline at end of file diff --git a/.agents/skills/pest-testing/SKILL.md b/.agents/skills/pest-testing/SKILL.md new file mode 100644 index 0000000..ba774e7 --- /dev/null +++ b/.agents/skills/pest-testing/SKILL.md @@ -0,0 +1,157 @@ +--- +name: pest-testing +description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code." +license: MIT +metadata: + author: laravel +--- + +# Pest Testing 4 + +## Documentation + +Use `search-docs` for detailed Pest 4 patterns and documentation. + +## Basic Usage + +### Creating Tests + +All tests must be written using Pest. Use `php artisan make:test --pest {name}`. + +### Test Organization + +- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories. +- Browser tests: `tests/Browser/` directory. +- Do NOT remove tests without approval - these are core application code. + +### Basic Test Structure + + +```php +it('is true', function () { + expect(true)->toBeTrue(); +}); +``` + +### Running Tests + +- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`. +- Run all tests: `php artisan test --compact`. +- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`. + +## Assertions + +Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`: + + +```php +it('returns all', function () { + $this->postJson('/api/docs', [])->assertSuccessful(); +}); +``` + +| Use | Instead of | +|-----|------------| +| `assertSuccessful()` | `assertStatus(200)` | +| `assertNotFound()` | `assertStatus(404)` | +| `assertForbidden()` | `assertStatus(403)` | + +## Mocking + +Import mock function before use: `use function Pest\Laravel\mock;` + +## Datasets + +Use datasets for repetitive tests (validation rules, etc.): + + +```php +it('has emails', function (string $email) { + expect($email)->not->toBeEmpty(); +})->with([ + 'james' => 'james@laravel.com', + 'taylor' => 'taylor@laravel.com', +]); +``` + +## Pest 4 Features + +| Feature | Purpose | +|---------|---------| +| Browser Testing | Full integration tests in real browsers | +| Smoke Testing | Validate multiple pages quickly | +| Visual Regression | Compare screenshots for visual changes | +| Test Sharding | Parallel CI runs | +| Architecture Testing | Enforce code conventions | + +### Browser Test Example + +Browser tests run in real browsers for full integration testing: + +- Browser tests live in `tests/Browser/`. +- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories. +- Use `RefreshDatabase` for clean state per test. +- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures. +- Test on multiple browsers (Chrome, Firefox, Safari) if requested. +- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested. +- Switch color schemes (light/dark mode) when appropriate. +- Take screenshots or pause tests for debugging. + + +```php +it('may reset the password', function () { + Notification::fake(); + + $this->actingAs(User::factory()->create()); + + $page = visit('/sign-in'); + + $page->assertSee('Sign In') + ->assertNoJavaScriptErrors() + ->click('Forgot Password?') + ->fill('email', 'nuno@laravel.com') + ->click('Send Reset Link') + ->assertSee('We have emailed your password reset link!'); + + Notification::assertSent(ResetPassword::class); +}); +``` + +### Smoke Testing + +Quickly validate multiple pages have no JavaScript errors: + + +```php +$pages = visit(['/', '/about', '/contact']); + +$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs(); +``` + +### Visual Regression Testing + +Capture and compare screenshots to detect visual changes. + +### Test Sharding + +Split tests across parallel processes for faster CI runs. + +### Architecture Testing + +Pest 4 includes architecture testing (from Pest 3): + + +```php +arch('controllers') + ->expect('App\Http\Controllers') + ->toExtendNothing() + ->toHaveSuffix('Controller'); +``` + +## Common Pitfalls + +- Not importing `use function Pest\Laravel\mock;` before using mock +- Using `assertStatus(200)` instead of `assertSuccessful()` +- Forgetting datasets for repetitive validation tests +- Deleting tests without approval +- Forgetting `assertNoJavaScriptErrors()` in browser tests \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/SKILL.md b/.claude/skills/laravel-best-practices/SKILL.md new file mode 100644 index 0000000..99018f3 --- /dev/null +++ b/.claude/skills/laravel-best-practices/SKILL.md @@ -0,0 +1,190 @@ +--- +name: laravel-best-practices +description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns." +license: MIT +metadata: + author: laravel +--- + +# Laravel Best Practices + +Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`. + +## Consistency First + +Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern. + +Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides. + +## Quick Reference + +### 1. Database Performance → `rules/db-performance.md` + +- Eager load with `with()` to prevent N+1 queries +- Enable `Model::preventLazyLoading()` in development +- Select only needed columns, avoid `SELECT *` +- `chunk()` / `chunkById()` for large datasets +- Index columns used in `WHERE`, `ORDER BY`, `JOIN` +- `withCount()` instead of loading relations to count +- `cursor()` for memory-efficient read-only iteration +- Never query in Blade templates + +### 2. Advanced Query Patterns → `rules/advanced-queries.md` + +- `addSelect()` subqueries over eager-loading entire has-many for a single value +- Dynamic relationships via subquery FK + `belongsTo` +- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries +- `setRelation()` to prevent circular N+1 queries +- `whereIn` + `pluck()` over `whereHas` for better index usage +- Two simple queries can beat one complex query +- Compound indexes matching `orderBy` column order +- Correlated subqueries in `orderBy` for has-many sorting (avoid joins) + +### 3. Security → `rules/security.md` + +- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates +- No raw SQL with user input — use Eloquent or query builder +- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes +- Validate MIME type, extension, and size for file uploads +- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields + +### 4. Caching → `rules/caching.md` + +- `Cache::remember()` over manual get/put +- `Cache::flexible()` for stale-while-revalidate on high-traffic data +- `Cache::memo()` to avoid redundant cache hits within a request +- Cache tags to invalidate related groups +- `Cache::add()` for atomic conditional writes +- `once()` to memoize per-request or per-object lifetime +- `Cache::lock()` / `lockForUpdate()` for race conditions +- Failover cache stores in production + +### 5. Eloquent Patterns → `rules/eloquent.md` + +- Correct relationship types with return type hints +- Local scopes for reusable query constraints +- Global scopes sparingly — document their existence +- Attribute casts in the `casts()` method +- Cast date columns, use Carbon instances in templates +- `whereBelongsTo($model)` for cleaner queries +- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries + +### 6. Validation & Forms → `rules/validation.md` + +- Form Request classes, not inline validation +- Array notation `['required', 'email']` for new code; follow existing convention +- `$request->validated()` only — never `$request->all()` +- `Rule::when()` for conditional validation +- `after()` instead of `withValidator()` + +### 7. Configuration → `rules/config.md` + +- `env()` only inside config files +- `App::environment()` or `app()->isProduction()` +- Config, lang files, and constants over hardcoded text + +### 8. Testing Patterns → `rules/testing.md` + +- `LazilyRefreshDatabase` over `RefreshDatabase` for speed +- `assertModelExists()` over raw `assertDatabaseHas()` +- Factory states and sequences over manual overrides +- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before +- `recycle()` to share relationship instances across factories + +### 9. Queue & Job Patterns → `rules/queue-jobs.md` + +- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]` +- `ShouldBeUnique` to prevent duplicates; `WithoutOverlapping::untilProcessing()` for concurrency +- Always implement `failed()`; with `retryUntil()`, set `$tries = 0` +- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs +- Horizon for complex multi-queue scenarios + +### 10. Routing & Controllers → `rules/routing.md` + +- Implicit route model binding +- Scoped bindings for nested resources +- `Route::resource()` or `apiResource()` +- Methods under 10 lines — extract to actions/services +- Type-hint Form Requests for auto-validation + +### 11. HTTP Client → `rules/http-client.md` + +- Explicit `timeout` and `connectTimeout` on every request +- `retry()` with exponential backoff for external APIs +- Check response status or use `throw()` +- `Http::pool()` for concurrent independent requests +- `Http::fake()` and `preventStrayRequests()` in tests + +### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md` + +- Event discovery over manual registration; `event:cache` in production +- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions +- Queue notifications and mailables with `ShouldQueue` +- On-demand notifications for non-user recipients +- `HasLocalePreference` on notifiable models +- `assertQueued()` not `assertSent()` for queued mailables +- Markdown mailables for transactional emails + +### 13. Error Handling → `rules/error-handling.md` + +- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern +- `ShouldntReport` for exceptions that should never log +- Throttle high-volume exceptions to protect log sinks +- `dontReportDuplicates()` for multi-catch scenarios +- Force JSON rendering for API routes +- Structured context via `context()` on exception classes + +### 14. Task Scheduling → `rules/scheduling.md` + +- `withoutOverlapping()` on variable-duration tasks +- `onOneServer()` on multi-server deployments +- `runInBackground()` for concurrent long tasks +- `environments()` to restrict to appropriate environments +- `takeUntilTimeout()` for time-bounded processing +- Schedule groups for shared configuration + +### 15. Architecture → `rules/architecture.md` + +- Single-purpose Action classes; dependency injection over `app()` helper +- Prefer official Laravel packages and follow conventions, don't override defaults +- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety +- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution + +### 16. Migrations → `rules/migrations.md` + +- Generate migrations with `php artisan make:migration` +- `constrained()` for foreign keys +- Never modify migrations that have run in production +- Add indexes in the migration, not as an afterthought +- Mirror column defaults in model `$attributes` +- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes +- One concern per migration — never mix DDL and DML + +### 17. Collections → `rules/collections.md` + +- Higher-order messages for simple collection operations +- `cursor()` vs. `lazy()` — choose based on relationship needs +- `lazyById()` when updating records while iterating +- `toQuery()` for bulk operations on collections + +### 18. Blade & Views → `rules/blade-views.md` + +- `$attributes->merge()` in component templates +- Blade components over `@include`; `@pushOnce` for per-component scripts +- View Composers for shared view data +- `@aware` for deeply nested component props + +### 19. Conventions & Style → `rules/style.md` + +- Follow Laravel naming conventions for all entities +- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions +- No JS/CSS in Blade, no HTML in PHP classes +- Code should be readable; comments only for config files + +## How to Apply + +Always use a sub-agent to read rule files and explore this skill's content. + +1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10) +2. Check sibling files for existing patterns — follow those first per Consistency First +3. Verify API syntax with `search-docs` for the installed Laravel version \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/advanced-queries.md b/.claude/skills/laravel-best-practices/rules/advanced-queries.md new file mode 100644 index 0000000..920714a --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/advanced-queries.md @@ -0,0 +1,106 @@ +# Advanced Query Patterns + +## Use `addSelect()` Subqueries for Single Values from Has-Many + +Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries. + +```php +public function scopeWithLastLoginAt($query): void +{ + $query->addSelect([ + 'last_login_at' => Login::select('created_at') + ->whereColumn('user_id', 'users.id') + ->latest() + ->take(1), + ])->withCasts(['last_login_at' => 'datetime']); +} +``` + +## Create Dynamic Relationships via Subquery FK + +Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection. + +```php +public function lastLogin(): BelongsTo +{ + return $this->belongsTo(Login::class); +} + +public function scopeWithLastLogin($query): void +{ + $query->addSelect([ + 'last_login_id' => Login::select('id') + ->whereColumn('user_id', 'users.id') + ->latest() + ->take(1), + ])->with('lastLogin'); +} +``` + +## Use Conditional Aggregates Instead of Multiple Count Queries + +Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values. + +```php +$statuses = Feature::toBase() + ->selectRaw("count(case when status = 'Requested' then 1 end) as requested") + ->selectRaw("count(case when status = 'Planned' then 1 end) as planned") + ->selectRaw("count(case when status = 'Completed' then 1 end) as completed") + ->first(); +``` + +## Use `setRelation()` to Prevent Circular N+1 + +When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries. + +```php +$feature->load('comments.user'); +$feature->comments->each->setRelation('feature', $feature); +``` + +## Prefer `whereIn` + Subquery Over `whereHas` + +`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory. + +Incorrect (correlated EXISTS re-executes per row): + +```php +$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term)); +``` + +Correct (index-friendly subquery, no PHP memory overhead): + +```php +$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id')); +``` + +## Sometimes Two Simple Queries Beat One Complex Query + +Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index. + +## Use Compound Indexes Matching `orderBy` Column Order + +When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index. + +```php +// Migration +$table->index(['last_name', 'first_name']); + +// Query — column order must match the index +User::query()->orderBy('last_name')->orderBy('first_name')->paginate(); +``` + +## Use Correlated Subqueries for Has-Many Ordering + +When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading. + +```php +public function scopeOrderByLastLogin($query): void +{ + $query->orderByDesc(Login::select('created_at') + ->whereColumn('user_id', 'users.id') + ->latest() + ->take(1) + ); +} +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/architecture.md b/.claude/skills/laravel-best-practices/rules/architecture.md new file mode 100644 index 0000000..1650564 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/architecture.md @@ -0,0 +1,202 @@ +# Architecture Best Practices + +## Single-Purpose Action Classes + +Extract discrete business operations into invokable Action classes. + +```php +class CreateOrderAction +{ + public function __construct(private InventoryService $inventory) {} + + public function execute(array $data): Order + { + $order = Order::create($data); + $this->inventory->reserve($order); + + return $order; + } +} +``` + +## Use Dependency Injection + +Always use constructor injection. Avoid `app()` or `resolve()` inside classes. + +Incorrect: +```php +class OrderController extends Controller +{ + public function store(StoreOrderRequest $request) + { + $service = app(OrderService::class); + + return $service->create($request->validated()); + } +} +``` + +Correct: +```php +class OrderController extends Controller +{ + public function __construct(private OrderService $service) {} + + public function store(StoreOrderRequest $request) + { + return $this->service->create($request->validated()); + } +} +``` + +## Code to Interfaces + +Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability. + +Incorrect (concrete dependency): +```php +class OrderService +{ + public function __construct(private StripeGateway $gateway) {} +} +``` + +Correct (interface dependency): +```php +interface PaymentGateway +{ + public function charge(int $amount, string $customerId): PaymentResult; +} + +class OrderService +{ + public function __construct(private PaymentGateway $gateway) {} +} +``` + +Bind in a service provider: + +```php +$this->app->bind(PaymentGateway::class, StripeGateway::class); +``` + +## Default Sort by Descending + +When no explicit order is specified, sort by `id` or `created_at` descending. Explicit ordering prevents cross-database inconsistencies between MySQL and Postgres. + +Incorrect: +```php +$posts = Post::paginate(); +``` + +Correct: +```php +$posts = Post::latest()->paginate(); +``` + +## Use Atomic Locks for Race Conditions + +Prevent race conditions with `Cache::lock()` or `lockForUpdate()`. + +```php +Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) { + $order->process(); +}); + +// Or at query level +$product = Product::where('id', $id)->lockForUpdate()->first(); +``` + +## Use `mb_*` String Functions + +When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters. + +Incorrect: +```php +strlen('José'); // 5 (bytes, not characters) +strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte +``` + +Correct: +```php +mb_strlen('José'); // 4 (characters) +mb_strtolower('MÜNCHEN'); // 'münchen' + +// Prefer Laravel's Str helpers when available +Str::length('José'); // 4 +Str::lower('MÜNCHEN'); // 'münchen' +``` + +## Use `defer()` for Post-Response Work + +For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead. + +Incorrect (job overhead for trivial work): +```php +dispatch(new LogPageView($page)); +``` + +Correct (runs after response, same process): +```php +defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()])); +``` + +Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work. + +## Use `Context` for Request-Scoped Data + +The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually. + +```php +// In middleware +Context::add('tenant_id', $request->header('X-Tenant-ID')); + +// Anywhere later — controllers, jobs, log context +$tenantId = Context::get('tenant_id'); +``` + +Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`. + +## Use `Concurrency::run()` for Parallel Execution + +Run independent operations in parallel using child processes — no async libraries needed. + +```php +use Illuminate\Support\Facades\Concurrency; + +[$users, $orders] = Concurrency::run([ + fn () => User::count(), + fn () => Order::where('status', 'pending')->count(), +]); +``` + +Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially. + +## Convention Over Configuration + +Follow Laravel conventions. Don't override defaults unnecessarily. + +Incorrect: +```php +class Customer extends Model +{ + protected $table = 'Customer'; + protected $primaryKey = 'customer_id'; + + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id'); + } +} +``` + +Correct: +```php +class Customer extends Model +{ + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class); + } +} +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/blade-views.md b/.claude/skills/laravel-best-practices/rules/blade-views.md new file mode 100644 index 0000000..c6f8aaf --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/blade-views.md @@ -0,0 +1,36 @@ +# Blade & Views Best Practices + +## Use `$attributes->merge()` in Component Templates + +Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly. + +```blade +
merge(['class' => 'alert alert-'.$type]) }}> + {{ $message }} +
+``` + +## Use `@pushOnce` for Per-Component Scripts + +If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once. + +## Prefer Blade Components Over `@include` + +`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots. + +## Use View Composers for Shared View Data + +If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it. + +## Use Blade Fragments for Partial Re-Renders (htmx/Turbo) + +A single view can return either the full page or just a fragment, keeping routing clean. + +```php +return view('dashboard', compact('users')) + ->fragmentIf($request->hasHeader('HX-Request'), 'user-list'); +``` + +## Use `@aware` for Deeply Nested Component Props + +Avoids re-passing parent props through every level of nested components. \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/caching.md b/.claude/skills/laravel-best-practices/rules/caching.md new file mode 100644 index 0000000..eb3ef3e --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/caching.md @@ -0,0 +1,70 @@ +# Caching Best Practices + +## Use `Cache::remember()` Instead of Manual Get/Put + +Atomic pattern prevents race conditions and removes boilerplate. + +Incorrect: +```php +$val = Cache::get('stats'); +if (! $val) { + $val = $this->computeStats(); + Cache::put('stats', $val, 60); +} +``` + +Correct: +```php +$val = Cache::remember('stats', 60, fn () => $this->computeStats()); +``` + +## Use `Cache::flexible()` for Stale-While-Revalidate + +On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background. + +Incorrect: `Cache::remember('users', 300, fn () => User::all());` + +Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function. + +## Use `Cache::memo()` to Avoid Redundant Hits Within a Request + +If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory. + +`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5. + +## Use Cache Tags to Invalidate Related Groups + +Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`. + +```php +Cache::tags(['user-1'])->flush(); +``` + +## Use `Cache::add()` for Atomic Conditional Writes + +`add()` only writes if the key does not exist — atomic, no race condition between checking and writing. + +Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }` + +Correct: `Cache::add('lock', true, 10);` + +## Use `once()` for Per-Request Memoization + +`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory. + +```php +public function roles(): Collection +{ + return once(fn () => $this->loadRoles()); +} +``` + +Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching. + +## Configure Failover Cache Stores in Production + +If Redis goes down, the app falls back to a secondary store automatically. + +```php +'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']], +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/collections.md b/.claude/skills/laravel-best-practices/rules/collections.md new file mode 100644 index 0000000..14f683d --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/collections.md @@ -0,0 +1,44 @@ +# Collection Best Practices + +## Use Higher-Order Messages for Simple Operations + +Incorrect: +```php +$users->each(function (User $user) { + $user->markAsVip(); +}); +``` + +Correct: `$users->each->markAsVip();` + +Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc. + +## Choose `cursor()` vs. `lazy()` Correctly + +- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk). +- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading. + +Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored. + +Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work. + +## Use `lazyById()` When Updating Records While Iterating + +`lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation. + +## Use `toQuery()` for Bulk Operations on Collections + +Avoids manual `whereIn` construction. + +Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);` + +Correct: `$users->toQuery()->update([...]);` + +## Use `#[CollectedBy]` for Custom Collection Classes + +More declarative than overriding `newCollection()`. + +```php +#[CollectedBy(UserCollection::class)] +class User extends Model {} +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/config.md b/.claude/skills/laravel-best-practices/rules/config.md new file mode 100644 index 0000000..8fd8f53 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/config.md @@ -0,0 +1,73 @@ +# Configuration Best Practices + +## `env()` Only in Config Files + +Direct `env()` calls return `null` when config is cached. + +Incorrect: +```php +$key = env('API_KEY'); +``` + +Correct: +```php +// config/services.php +'key' => env('API_KEY'), + +// Application code +$key = config('services.key'); +``` + +## Use Encrypted Env or External Secrets + +Never store production secrets in plain `.env` files in version control. + +Incorrect: +```bash + +# .env committed to repo or shared in Slack + +STRIPE_SECRET=sk_live_abc123 +AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI +``` + +Correct: +```bash +php artisan env:encrypt --env=production --readable +php artisan env:decrypt --env=production +``` + +For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime. + +## Use `App::environment()` for Environment Checks + +Incorrect: +```php +if (env('APP_ENV') === 'production') { +``` + +Correct: +```php +if (app()->isProduction()) { +// or +if (App::environment('production')) { +``` + +## Use Constants and Language Files + +Use class constants instead of hardcoded magic strings for model states, types, and statuses. + +```php +// Incorrect +return $this->type === 'normal'; + +// Correct +return $this->type === self::TYPE_NORMAL; +``` + +If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there. + +```php +// Only when lang files already exist in the project +return back()->with('message', __('app.article_added')); +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/db-performance.md b/.claude/skills/laravel-best-practices/rules/db-performance.md new file mode 100644 index 0000000..8fb7193 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/db-performance.md @@ -0,0 +1,192 @@ +# Database Performance Best Practices + +## Always Eager Load Relationships + +Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront. + +Incorrect (N+1 — executes 1 + N queries): +```php +$posts = Post::all(); +foreach ($posts as $post) { + echo $post->author->name; +} +``` + +Correct (2 queries total): +```php +$posts = Post::with('author')->get(); +foreach ($posts as $post) { + echo $post->author->name; +} +``` + +Constrain eager loads to select only needed columns (always include the foreign key): + +```php +$users = User::with(['posts' => function ($query) { + $query->select('id', 'user_id', 'title') + ->where('published', true) + ->latest() + ->limit(10); +}])->get(); +``` + +## Prevent Lazy Loading in Development + +Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development. + +```php +public function boot(): void +{ + Model::preventLazyLoading(! app()->isProduction()); +} +``` + +Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded. + +## Select Only Needed Columns + +Avoid `SELECT *` — especially when tables have large text or JSON columns. + +Incorrect: +```php +$posts = Post::with('author')->get(); +``` + +Correct: +```php +$posts = Post::select('id', 'title', 'user_id', 'created_at') + ->with(['author:id,name,avatar']) + ->get(); +``` + +When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match. + +## Chunk Large Datasets + +Never load thousands of records at once. Use chunking for batch processing. + +Incorrect: +```php +$users = User::all(); +foreach ($users as $user) { + $user->notify(new WeeklyDigest); +} +``` + +Correct: +```php +User::where('subscribed', true)->chunk(200, function ($users) { + foreach ($users as $user) { + $user->notify(new WeeklyDigest); + } +}); +``` + +Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change: + +```php +User::where('active', false)->chunkById(200, function ($users) { + $users->each->delete(); +}); +``` + +## Add Database Indexes + +Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses. + +Incorrect: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained(); + $table->string('status'); + $table->timestamps(); +}); +``` + +Correct: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->index()->constrained(); + $table->string('status')->index(); + $table->timestamps(); + $table->index(['status', 'created_at']); +}); +``` + +Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`). + +## Use `withCount()` for Counting Relations + +Never load entire collections just to count them. + +Incorrect: +```php +$posts = Post::all(); +foreach ($posts as $post) { + echo $post->comments->count(); +} +``` + +Correct: +```php +$posts = Post::withCount('comments')->get(); +foreach ($posts as $post) { + echo $post->comments_count; +} +``` + +Conditional counting: + +```php +$posts = Post::withCount([ + 'comments', + 'comments as approved_comments_count' => function ($query) { + $query->where('approved', true); + }, +])->get(); +``` + +## Use `cursor()` for Memory-Efficient Iteration + +For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator. + +Incorrect: +```php +$users = User::where('active', true)->get(); +``` + +Correct: +```php +foreach (User::where('active', true)->cursor() as $user) { + ProcessUser::dispatch($user->id); +} +``` + +Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records. + +## No Queries in Blade Templates + +Never execute queries in Blade templates. Pass data from controllers. + +Incorrect: +```blade +@foreach (User::all() as $user) + {{ $user->profile->name }} +@endforeach +``` + +Correct: +```php +// Controller +$users = User::with('profile')->get(); +return view('users.index', compact('users')); +``` + +```blade +@foreach ($users as $user) + {{ $user->profile->name }} +@endforeach +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/eloquent.md b/.claude/skills/laravel-best-practices/rules/eloquent.md new file mode 100644 index 0000000..09cd66a --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/eloquent.md @@ -0,0 +1,148 @@ +# Eloquent Best Practices + +## Use Correct Relationship Types + +Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints. + +```php +public function comments(): HasMany +{ + return $this->hasMany(Comment::class); +} + +public function author(): BelongsTo +{ + return $this->belongsTo(User::class, 'user_id'); +} +``` + +## Use Local Scopes for Reusable Queries + +Extract reusable query constraints into local scopes to avoid duplication. + +Incorrect: +```php +$active = User::where('verified', true)->whereNotNull('activated_at')->get(); +$articles = Article::whereHas('user', function ($q) { + $q->where('verified', true)->whereNotNull('activated_at'); +})->get(); +``` + +Correct: +```php +public function scopeActive(Builder $query): Builder +{ + return $query->where('verified', true)->whereNotNull('activated_at'); +} + +// Usage +$active = User::active()->get(); +$articles = Article::whereHas('user', fn ($q) => $q->active())->get(); +``` + +## Apply Global Scopes Sparingly + +Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy. + +Incorrect (global scope for a conditional filter): +```php +class PublishedScope implements Scope +{ + public function apply(Builder $builder, Model $model): void + { + $builder->where('published', true); + } +} +// Now admin panels, reports, and background jobs all silently skip drafts +``` + +Correct (local scope you opt into): +```php +public function scopePublished(Builder $query): Builder +{ + return $query->where('published', true); +} + +Post::published()->paginate(); // Explicit +Post::paginate(); // Admin sees all +``` + +## Define Attribute Casts + +Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion. + +```php +protected function casts(): array +{ + return [ + 'is_active' => 'boolean', + 'metadata' => 'array', + 'total' => 'decimal:2', + ]; +} +``` + +## Cast Date Columns Properly + +Always cast date columns. Use Carbon instances in templates instead of formatting strings manually. + +Incorrect: +```blade +{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }} +``` + +Correct: +```php +protected function casts(): array +{ + return [ + 'ordered_at' => 'datetime', + ]; +} +``` + +```blade +{{ $order->ordered_at->toDateString() }} +{{ $order->ordered_at->format('m-d') }} +``` + +## Use `whereBelongsTo()` for Relationship Queries + +Cleaner than manually specifying foreign keys. + +Incorrect: +```php +Post::where('user_id', $user->id)->get(); +``` + +Correct: +```php +Post::whereBelongsTo($user)->get(); +Post::whereBelongsTo($user, 'author')->get(); +``` + +## Avoid Hardcoded Table Names in Queries + +Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string). + +Incorrect: +```php +DB::table('users')->where('active', true)->get(); + +$query->join('companies', 'companies.id', '=', 'users.company_id'); + +DB::select('SELECT * FROM orders WHERE status = ?', ['pending']); +``` + +Correct — reference the model's table: +```php +DB::table((new User)->getTable())->where('active', true)->get(); + +// Even better — use Eloquent or the query builder instead of raw SQL +User::where('active', true)->get(); +Order::where('status', 'pending')->get(); +``` + +Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable. + +**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration. \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/error-handling.md b/.claude/skills/laravel-best-practices/rules/error-handling.md new file mode 100644 index 0000000..bb8e7a3 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/error-handling.md @@ -0,0 +1,72 @@ +# Error Handling Best Practices + +## Exception Reporting and Rendering + +There are two valid approaches — choose one and apply it consistently across the project. + +**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find: + +```php +class InvalidOrderException extends Exception +{ + public function report(): void { /* custom reporting */ } + + public function render(Request $request): Response + { + return response()->view('errors.invalid-order', status: 422); + } +} +``` + +**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture: + +```php +->withExceptions(function (Exceptions $exceptions) { + $exceptions->report(function (InvalidOrderException $e) { /* ... */ }); + $exceptions->render(function (InvalidOrderException $e, Request $request) { + return response()->view('errors.invalid-order', status: 422); + }); +}) +``` + +Check the existing codebase and follow whichever pattern is already established. + +## Use `ShouldntReport` for Exceptions That Should Never Log + +More discoverable than listing classes in `dontReport()`. + +```php +class PodcastProcessingException extends Exception implements ShouldntReport {} +``` + +## Throttle High-Volume Exceptions + +A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type. + +## Enable `dontReportDuplicates()` + +Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks. + +## Force JSON Error Rendering for API Routes + +Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes. + +```php +$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { + return $request->is('api/*') || $request->expectsJson(); +}); +``` + +## Add Context to Exception Classes + +Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry. + +```php +class InvalidOrderException extends Exception +{ + public function context(): array + { + return ['order_id' => $this->orderId]; + } +} +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/events-notifications.md b/.claude/skills/laravel-best-practices/rules/events-notifications.md new file mode 100644 index 0000000..bc43f19 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/events-notifications.md @@ -0,0 +1,48 @@ +# Events & Notifications Best Practices + +## Rely on Event Discovery + +Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`. + +## Run `event:cache` in Production Deploy + +Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`. + +## Use `ShouldDispatchAfterCommit` Inside Transactions + +Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet. + +```php +class OrderShipped implements ShouldDispatchAfterCommit {} +``` + +## Always Queue Notifications + +Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response. + +```php +class InvoicePaid extends Notification implements ShouldQueue +{ + use Queueable; +} +``` + +## Use `afterCommit()` on Notifications in Transactions + +Same race condition as events — the queued notification job may run before the transaction commits. + +## Route Notification Channels to Dedicated Queues + +Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues. + +## Use On-Demand Notifications for Non-User Recipients + +Avoid creating dummy models to send notifications to arbitrary addresses. + +```php +Notification::route('mail', 'admin@example.com')->notify(new SystemAlert()); +``` + +## Implement `HasLocalePreference` on Notifiable Models + +Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed. \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/http-client.md b/.claude/skills/laravel-best-practices/rules/http-client.md new file mode 100644 index 0000000..0a7876e --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/http-client.md @@ -0,0 +1,160 @@ +# HTTP Client Best Practices + +## Always Set Explicit Timeouts + +The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast. + +Incorrect: +```php +$response = Http::get('https://api.example.com/users'); +``` + +Correct: +```php +$response = Http::timeout(5) + ->connectTimeout(3) + ->get('https://api.example.com/users'); +``` + +For service-specific clients, define timeouts in a macro: + +```php +Http::macro('github', function () { + return Http::baseUrl('https://api.github.com') + ->timeout(10) + ->connectTimeout(3) + ->withToken(config('services.github.token')); +}); + +$response = Http::github()->get('/repos/laravel/framework'); +``` + +## Use Retry with Backoff for External APIs + +External APIs have transient failures. Use `retry()` with increasing delays. + +Incorrect: +```php +$response = Http::post('https://api.stripe.com/v1/charges', $data); + +if ($response->failed()) { + throw new PaymentFailedException('Charge failed'); +} +``` + +Correct: +```php +$response = Http::retry([100, 500, 1000]) + ->timeout(10) + ->post('https://api.stripe.com/v1/charges', $data); +``` + +Only retry on specific errors: + +```php +$response = Http::retry(3, 100, function (Exception $exception, PendingRequest $request) { + return $exception instanceof ConnectionException + || ($exception instanceof RequestException && $exception->response->serverError()); +})->post('https://api.example.com/data'); +``` + +## Handle Errors Explicitly + +The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`. + +Incorrect: +```php +$response = Http::get('https://api.example.com/users/1'); +$user = $response->json(); // Could be an error body +``` + +Correct: +```php +$response = Http::timeout(5) + ->get('https://api.example.com/users/1') + ->throw(); + +$user = $response->json(); +``` + +For graceful degradation: + +```php +$response = Http::get('https://api.example.com/users/1'); + +if ($response->successful()) { + return $response->json(); +} + +if ($response->notFound()) { + return null; +} + +$response->throw(); +``` + +## Use Request Pooling for Concurrent Requests + +When making multiple independent API calls, use `Http::pool()` instead of sequential calls. + +Incorrect: +```php +$users = Http::get('https://api.example.com/users')->json(); +$posts = Http::get('https://api.example.com/posts')->json(); +$comments = Http::get('https://api.example.com/comments')->json(); +``` + +Correct: +```php +use Illuminate\Http\Client\Pool; + +$responses = Http::pool(fn (Pool $pool) => [ + $pool->as('users')->get('https://api.example.com/users'), + $pool->as('posts')->get('https://api.example.com/posts'), + $pool->as('comments')->get('https://api.example.com/comments'), +]); + +$users = $responses['users']->json(); +$posts = $responses['posts']->json(); +``` + +## Fake HTTP Calls in Tests + +Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`. + +Incorrect: +```php +it('syncs user from API', function () { + $service = new UserSyncService; + $service->sync(1); // Hits the real API +}); +``` + +Correct: +```php +it('syncs user from API', function () { + Http::preventStrayRequests(); + + Http::fake([ + 'api.example.com/users/1' => Http::response([ + 'name' => 'John Doe', + 'email' => 'john@example.com', + ]), + ]); + + $service = new UserSyncService; + $service->sync(1); + + Http::assertSent(function (Request $request) { + return $request->url() === 'https://api.example.com/users/1'; + }); +}); +``` + +Test failure scenarios too: + +```php +Http::fake([ + 'api.example.com/*' => Http::failedConnection(), +]); +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/mail.md b/.claude/skills/laravel-best-practices/rules/mail.md new file mode 100644 index 0000000..c7f6796 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/mail.md @@ -0,0 +1,27 @@ +# Mail Best Practices + +## Implement `ShouldQueue` on the Mailable Class + +Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it. + +## Use `afterCommit()` on Mailables Inside Transactions + +A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor. + +## Use `assertQueued()` Not `assertSent()` for Queued Mailables + +`Mail::assertSent()` only catches synchronous mail. Queued mailables silently pass `assertSent`, giving false confidence. + +Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`. + +Correct: `Mail::assertQueued(OrderShipped::class);` + +## Use Markdown Mailables for Transactional Emails + +Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag. + +## Separate Content Tests from Sending Tests + +Content tests: instantiate the mailable directly, call `assertSeeInHtml()`. +Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`. +Don't mix them — it conflates concerns and makes tests brittle. \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/migrations.md b/.claude/skills/laravel-best-practices/rules/migrations.md new file mode 100644 index 0000000..de25aa3 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/migrations.md @@ -0,0 +1,121 @@ +# Migration Best Practices + +## Generate Migrations with Artisan + +Always use `php artisan make:migration` for consistent naming and timestamps. + +Incorrect (manually created file): +```php +// database/migrations/posts_migration.php ← wrong naming, no timestamp +``` + +Correct (Artisan-generated): +```bash +php artisan make:migration create_posts_table +php artisan make:migration add_slug_to_posts_table +``` + +## Use `constrained()` for Foreign Keys + +Automatic naming and referential integrity. + +```php +$table->foreignId('user_id')->constrained()->cascadeOnDelete(); + +// Non-standard names +$table->foreignId('author_id')->constrained('users'); +``` + +## Never Modify Deployed Migrations + +Once a migration has run in production, treat it as immutable. Create a new migration to change the table. + +Incorrect (editing a deployed migration): +```php +// 2024_01_01_create_posts_table.php — already in production +$table->string('slug')->unique(); // ← added after deployment +``` + +Correct (new migration to alter): +```php +// 2024_03_15_add_slug_to_posts_table.php +Schema::table('posts', function (Blueprint $table) { + $table->string('slug')->unique()->after('title'); +}); +``` + +## Add Indexes in the Migration + +Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes. + +Incorrect: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained(); + $table->string('status'); + $table->timestamps(); +}); +``` + +Correct: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained()->index(); + $table->string('status')->index(); + $table->timestamp('shipped_at')->nullable()->index(); + $table->timestamps(); +}); +``` + +## Mirror Defaults in Model `$attributes` + +When a column has a database default, mirror it in the model so new instances have correct values before saving. + +```php +// Migration +$table->string('status')->default('pending'); + +// Model +protected $attributes = [ + 'status' => 'pending', +]; +``` + +## Write Reversible `down()` Methods by Default + +Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments. + +```php +public function down(): void +{ + Schema::table('posts', function (Blueprint $table) { + $table->dropColumn('slug'); + }); +} +``` + +For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported. + +## Keep Migrations Focused + +One concern per migration. Never mix DDL (schema changes) and DML (data manipulation). + +Incorrect (partial failure creates unrecoverable state): +```php +public function up(): void +{ + Schema::create('settings', function (Blueprint $table) { ... }); + DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']); +} +``` + +Correct (separate migrations): +```php +// Migration 1: create_settings_table +Schema::create('settings', function (Blueprint $table) { ... }); + +// Migration 2: seed_default_settings +DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']); +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/queue-jobs.md b/.claude/skills/laravel-best-practices/rules/queue-jobs.md new file mode 100644 index 0000000..d4575aa --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/queue-jobs.md @@ -0,0 +1,146 @@ +# Queue & Job Best Practices + +## Set `retry_after` Greater Than `timeout` + +If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution. + +Incorrect (`retry_after` ≤ `timeout`): +```php +class ProcessReport implements ShouldQueue +{ + public $timeout = 120; +} + +// config/queue.php — retry_after: 90 ← job retried while still running! +``` + +Correct (`retry_after` > `timeout`): +```php +class ProcessReport implements ShouldQueue +{ + public $timeout = 120; +} + +// config/queue.php — retry_after: 180 ← safely longer than any job timeout +``` + +## Use Exponential Backoff + +Use progressively longer delays between retries to avoid hammering failing services. + +Incorrect (fixed retry interval): +```php +class SyncWithStripe implements ShouldQueue +{ + public $tries = 3; + // Default: retries immediately, overwhelming the API +} +``` + +Correct (exponential backoff): +```php +class SyncWithStripe implements ShouldQueue +{ + public $tries = 3; + public $backoff = [1, 5, 10]; +} +``` + +## Implement `ShouldBeUnique` + +Prevent duplicate job processing. + +```php +class GenerateInvoice implements ShouldQueue, ShouldBeUnique +{ + public function uniqueId(): string + { + return $this->order->id; + } + + public $uniqueFor = 3600; +} +``` + +## Always Implement `failed()` + +Handle errors explicitly — don't rely on silent failure. + +```php +public function failed(?Throwable $exception): void +{ + $this->podcast->update(['status' => 'failed']); + Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]); +} +``` + +## Rate Limit External API Calls in Jobs + +Use `RateLimited` middleware to throttle jobs calling third-party APIs. + +```php +public function middleware(): array +{ + return [new RateLimited('external-api')]; +} +``` + +## Batch Related Jobs + +Use `Bus::batch()` when jobs should succeed or fail together. + +```php +Bus::batch([ + new ImportCsvChunk($chunk1), + new ImportCsvChunk($chunk2), +]) +->then(fn (Batch $batch) => Notification::send($user, new ImportComplete)) +->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed')) +->dispatch(); +``` + +## `retryUntil()` Needs `$tries = 0` + +When using time-based retry limits, set `$tries = 0` to avoid premature failure. + +```php +public $tries = 0; + +public function retryUntil(): DateTime +{ + return now()->addHours(4); +} +``` + +## Use `WithoutOverlapping::untilProcessing()` + +Prevents concurrent execution while allowing new instances to queue. + +```php +public function middleware(): array +{ + return [new WithoutOverlapping($this->product->id)->untilProcessing()]; +} +``` + +Without `untilProcessing()`, the lock extends through queue wait time. With it, the lock releases when processing starts. + +## Use Horizon for Complex Queue Scenarios + +Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities. + +```php +// config/horizon.php +'environments' => [ + 'production' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['high', 'default', 'low'], + 'balance' => 'auto', + 'minProcesses' => 1, + 'maxProcesses' => 10, + 'tries' => 3, + ], + ], +], +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/routing.md b/.claude/skills/laravel-best-practices/rules/routing.md new file mode 100644 index 0000000..e288375 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/routing.md @@ -0,0 +1,98 @@ +# Routing & Controllers Best Practices + +## Use Implicit Route Model Binding + +Let Laravel resolve models automatically from route parameters. + +Incorrect: +```php +public function show(int $id) +{ + $post = Post::findOrFail($id); +} +``` + +Correct: +```php +public function show(Post $post) +{ + return view('posts.show', ['post' => $post]); +} +``` + +## Use Scoped Bindings for Nested Resources + +Enforce parent-child relationships automatically. + +```php +Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) { + // $post is automatically scoped to $user +})->scopeBindings(); +``` + +## Use Resource Controllers + +Use `Route::resource()` or `apiResource()` for RESTful endpoints. + +```php +Route::resource('posts', PostController::class); +Route::apiResource('api/posts', Api\PostController::class); +``` + +## Keep Controllers Thin + +Aim for under 10 lines per method. Extract business logic to action or service classes. + +Incorrect: +```php +public function store(Request $request) +{ + $validated = $request->validate([...]); + if ($request->hasFile('image')) { + $request->file('image')->move(public_path('images')); + } + $post = Post::create($validated); + $post->tags()->sync($validated['tags']); + event(new PostCreated($post)); + return redirect()->route('posts.show', $post); +} +``` + +Correct: +```php +public function store(StorePostRequest $request, CreatePostAction $create) +{ + $post = $create->execute($request->validated()); + + return redirect()->route('posts.show', $post); +} +``` + +## Type-Hint Form Requests + +Type-hinting Form Requests triggers automatic validation and authorization before the method executes. + +Incorrect: +```php +public function store(Request $request): RedirectResponse +{ + $validated = $request->validate([ + 'title' => ['required', 'max:255'], + 'body' => ['required'], + ]); + + Post::create($validated); + + return redirect()->route('posts.index'); +} +``` + +Correct: +```php +public function store(StorePostRequest $request): RedirectResponse +{ + Post::create($request->validated()); + + return redirect()->route('posts.index'); +} +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/scheduling.md b/.claude/skills/laravel-best-practices/rules/scheduling.md new file mode 100644 index 0000000..dfaefa2 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/scheduling.md @@ -0,0 +1,39 @@ +# Task Scheduling Best Practices + +## Use `withoutOverlapping()` on Variable-Duration Tasks + +Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion. + +## Use `onOneServer()` on Multi-Server Deployments + +Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached). + +## Use `runInBackground()` for Concurrent Long Tasks + +By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes. + +## Use `environments()` to Restrict Tasks + +Prevent accidental execution of production-only tasks (billing, reporting) on staging. + +```php +Schedule::command('billing:charge')->monthly()->environments(['production']); +``` + +## Use `takeUntilTimeout()` for Time-Bounded Processing + +A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time. + +## Use Schedule Groups for Shared Configuration + +Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks. + +```php +Schedule::daily() + ->onOneServer() + ->timezone('America/New_York') + ->group(function () { + Schedule::command('emails:send --force'); + Schedule::command('emails:prune'); + }); +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/security.md b/.claude/skills/laravel-best-practices/rules/security.md new file mode 100644 index 0000000..524d47e --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/security.md @@ -0,0 +1,198 @@ +# Security Best Practices + +## Mass Assignment Protection + +Every model must define `$fillable` (whitelist) or `$guarded` (blacklist). + +Incorrect: +```php +class User extends Model +{ + protected $guarded = []; // All fields are mass assignable +} +``` + +Correct: +```php +class User extends Model +{ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; +} +``` + +Never use `$guarded = []` on models that accept user input. + +## Authorize Every Action + +Use policies or gates in controllers. Never skip authorization. + +Incorrect: +```php +public function update(Request $request, Post $post) +{ + $post->update($request->validated()); +} +``` + +Correct: +```php +public function update(UpdatePostRequest $request, Post $post) +{ + Gate::authorize('update', $post); + + $post->update($request->validated()); +} +``` + +Or via Form Request: + +```php +public function authorize(): bool +{ + return $this->user()->can('update', $this->route('post')); +} +``` + +## Prevent SQL Injection + +Always use parameter binding. Never interpolate user input into queries. + +Incorrect: +```php +DB::select("SELECT * FROM users WHERE name = '{$request->name}'"); +``` + +Correct: +```php +User::where('name', $request->name)->get(); + +// Raw expressions with bindings +User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get(); +``` + +## Escape Output to Prevent XSS + +Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content. + +Incorrect: +```blade +{!! $user->bio !!} +``` + +Correct: +```blade +{{ $user->bio }} +``` + +## CSRF Protection + +Include `@csrf` in all POST/PUT/DELETE Blade forms. Not needed in Inertia. + +Incorrect: +```blade +
+ +
+``` + +Correct: +```blade +
+ @csrf + +
+``` + +## Rate Limit Auth and API Routes + +Apply `throttle` middleware to authentication and API routes. + +```php +RateLimiter::for('login', function (Request $request) { + return Limit::perMinute(5)->by($request->ip()); +}); + +Route::post('/login', LoginController::class)->middleware('throttle:login'); +``` + +## Validate File Uploads + +Validate MIME type, extension, and size. Never trust client-provided filenames. + +```php +public function rules(): array +{ + return [ + 'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'], + ]; +} +``` + +Store with generated filenames: + +```php +$path = $request->file('avatar')->store('avatars', 'public'); +``` + +## Keep Secrets Out of Code + +Never commit `.env`. Access secrets via `config()` only. + +Incorrect: +```php +$key = env('API_KEY'); +``` + +Correct: +```php +// config/services.php +'api_key' => env('API_KEY'), + +// In application code +$key = config('services.api_key'); +``` + +## Audit Dependencies + +Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment. + +```bash +composer audit +``` + +## Encrypt Sensitive Database Fields + +Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`. + +Incorrect: +```php +class Integration extends Model +{ + protected function casts(): array + { + return [ + 'api_key' => 'string', + ]; + } +} +``` + +Correct: +```php +class Integration extends Model +{ + protected $hidden = ['api_key', 'api_secret']; + + protected function casts(): array + { + return [ + 'api_key' => 'encrypted', + 'api_secret' => 'encrypted', + ]; + } +} +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/style.md b/.claude/skills/laravel-best-practices/rules/style.md new file mode 100644 index 0000000000000000000000000000000000000000..db689bf774d1763ac3ec520a3874015f5421ff5b GIT binary patch literal 4443 zcmb7H|8g6*5$@mj6gxJXBU_TPGV!!SM{H%Ls*_r-YD&%@j)w&AKoK^0I0HCRY@C@s zM4zxv(rqc3Rg4_YS46<&Z-l2$9V!-oHYUfv=K_C|PowZt|LcB75;$1eTUe78Vh&a+E%-f!B^_a3OyuOh!1j+3CF~xh&C6Q z*=`XQmT8HzBMo|P)XsSFwYJu8q05a}Nq8>Un^s}fxWXTc+D!ClW^}bJz?`D4gwP={HL5A^SnD#A?)(C5LRZ zR@zG|^YKcHT#n048H9Q7>X){{QHr&?hq_KiVE^8jd(B0!WswWps*3d4DH&@1R8(75 z(o_>v@X2ovWz1l+2v>~J<^>HjOLin4++uISp>Q7sc= zww6}<$`&|bt}L=XnXE+ip&z}g_gV`3HWN5EPEweC&DDJI?qyj{CR+efKb>jeTy0sD zWtYI5qv?KwV!+7*dZa^2FYxC)TK@TN*ocD0=F&btK-5a%Wxf!e#ktaJd!wnwhV#M0 z|0*OpGDbs1S7xm&uSe55UhMTw=n7u9VGYd&_0x8mxwqVDzMxBM#erT(T>>Yi70@@Er6gkS%swHiLEM*)?2zc&R!b$)u{^0DPCWn-5getf^ zqwL-7)#&%+#9FdML00VP=EV)It0I7c8`GuUi-V&wR=MBE?KnxIkV45w0(RotSq(2 z5JBRcjqs-zn!;f43?h8rSf*Nmx8L*f!4K&P%HqkB0gWjgkH;zaLPU;y;I-Mt_EVJK z5225`U*USQfgjQV7uBs9|qK z8N(d$vlHhUP>F9>t{hc! zpk(1NTthYH##aU%kYRXf1Z5TJ#a|1EE=6q<>`nN5$zBCL1$xV1li$&!Wm{EWjJgF+mOY z2m}FlAue$xtpY2C90Ue9gpB9NU51F{eEN|$BM6k3O;JN_tnZ3KW*Cu$J)pVA7jKfx zaAd+LQR$pk$3cm3G=+DZ*%xEtGUz;w>gE-uon7-1V<>b8zw_u7Tofqy@TeZsE$W5A zjUuG+Q%gDQht~?1jCf*ZqtdaFudezoDD5U(J5b>JS*l%5P?3c6WmL#(LhH_DBsfbQ>Dd4CXi F{|%Ge#T5Vm literal 0 HcmV?d00001 diff --git a/.claude/skills/laravel-best-practices/rules/testing.md b/.claude/skills/laravel-best-practices/rules/testing.md new file mode 100644 index 0000000..d39cc3e --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/testing.md @@ -0,0 +1,43 @@ +# Testing Best Practices + +## Use `LazilyRefreshDatabase` Over `RefreshDatabase` + +`RefreshDatabase` runs all migrations every test run even when the schema hasn't changed. `LazilyRefreshDatabase` only migrates when needed, significantly speeding up large suites. + +## Use Model Assertions Over Raw Database Assertions + +Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);` + +Correct: `$this->assertModelExists($user);` + +More expressive, type-safe, and fails with clearer messages. + +## Use Factory States and Sequences + +Named states make tests self-documenting. Sequences eliminate repetitive setup. + +Incorrect: `User::factory()->create(['email_verified_at' => null]);` + +Correct: `User::factory()->unverified()->create();` + +## Use `Exceptions::fake()` to Assert Exception Reporting + +Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally. + +## Call `Event::fake()` After Factory Setup + +Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models. + +Incorrect: `Event::fake(); $user = User::factory()->create();` + +Correct: `$user = User::factory()->create(); Event::fake();` + +## Use `recycle()` to Share Relationship Instances Across Factories + +Without `recycle()`, nested factories create separate instances of the same conceptual entity. + +```php +Ticket::factory() + ->recycle(Airline::factory()->create()) + ->create(); +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/validation.md b/.claude/skills/laravel-best-practices/rules/validation.md new file mode 100644 index 0000000..a20202f --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/validation.md @@ -0,0 +1,75 @@ +# Validation & Forms Best Practices + +## Use Form Request Classes + +Extract validation from controllers into dedicated Form Request classes. + +Incorrect: +```php +public function store(Request $request) +{ + $request->validate([ + 'title' => 'required|max:255', + 'body' => 'required', + ]); +} +``` + +Correct: +```php +public function store(StorePostRequest $request) +{ + Post::create($request->validated()); +} +``` + +## Array vs. String Notation for Rules + +Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses. + +```php +// Preferred for new code +'email' => ['required', 'email', Rule::unique('users')], + +// Follow existing convention if the project uses string notation +'email' => 'required|email|unique:users', +``` + +## Always Use `validated()` + +Get only validated data. Never use `$request->all()` for mass operations. + +Incorrect: +```php +Post::create($request->all()); +``` + +Correct: +```php +Post::create($request->validated()); +``` + +## Use `Rule::when()` for Conditional Validation + +```php +'company_name' => [ + Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']), +], +``` + +## Use the `after()` Method for Custom Validation + +Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields. + +```php +public function after(): array +{ + return [ + function (Validator $validator) { + if ($this->quantity > Product::find($this->product_id)?->stock) { + $validator->errors()->add('quantity', 'Not enough stock.'); + } + }, + ]; +} +``` \ No newline at end of file diff --git a/.claude/skills/pest-testing/SKILL.md b/.claude/skills/pest-testing/SKILL.md new file mode 100644 index 0000000..ba774e7 --- /dev/null +++ b/.claude/skills/pest-testing/SKILL.md @@ -0,0 +1,157 @@ +--- +name: pest-testing +description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code." +license: MIT +metadata: + author: laravel +--- + +# Pest Testing 4 + +## Documentation + +Use `search-docs` for detailed Pest 4 patterns and documentation. + +## Basic Usage + +### Creating Tests + +All tests must be written using Pest. Use `php artisan make:test --pest {name}`. + +### Test Organization + +- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories. +- Browser tests: `tests/Browser/` directory. +- Do NOT remove tests without approval - these are core application code. + +### Basic Test Structure + + +```php +it('is true', function () { + expect(true)->toBeTrue(); +}); +``` + +### Running Tests + +- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`. +- Run all tests: `php artisan test --compact`. +- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`. + +## Assertions + +Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`: + + +```php +it('returns all', function () { + $this->postJson('/api/docs', [])->assertSuccessful(); +}); +``` + +| Use | Instead of | +|-----|------------| +| `assertSuccessful()` | `assertStatus(200)` | +| `assertNotFound()` | `assertStatus(404)` | +| `assertForbidden()` | `assertStatus(403)` | + +## Mocking + +Import mock function before use: `use function Pest\Laravel\mock;` + +## Datasets + +Use datasets for repetitive tests (validation rules, etc.): + + +```php +it('has emails', function (string $email) { + expect($email)->not->toBeEmpty(); +})->with([ + 'james' => 'james@laravel.com', + 'taylor' => 'taylor@laravel.com', +]); +``` + +## Pest 4 Features + +| Feature | Purpose | +|---------|---------| +| Browser Testing | Full integration tests in real browsers | +| Smoke Testing | Validate multiple pages quickly | +| Visual Regression | Compare screenshots for visual changes | +| Test Sharding | Parallel CI runs | +| Architecture Testing | Enforce code conventions | + +### Browser Test Example + +Browser tests run in real browsers for full integration testing: + +- Browser tests live in `tests/Browser/`. +- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories. +- Use `RefreshDatabase` for clean state per test. +- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures. +- Test on multiple browsers (Chrome, Firefox, Safari) if requested. +- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested. +- Switch color schemes (light/dark mode) when appropriate. +- Take screenshots or pause tests for debugging. + + +```php +it('may reset the password', function () { + Notification::fake(); + + $this->actingAs(User::factory()->create()); + + $page = visit('/sign-in'); + + $page->assertSee('Sign In') + ->assertNoJavaScriptErrors() + ->click('Forgot Password?') + ->fill('email', 'nuno@laravel.com') + ->click('Send Reset Link') + ->assertSee('We have emailed your password reset link!'); + + Notification::assertSent(ResetPassword::class); +}); +``` + +### Smoke Testing + +Quickly validate multiple pages have no JavaScript errors: + + +```php +$pages = visit(['/', '/about', '/contact']); + +$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs(); +``` + +### Visual Regression Testing + +Capture and compare screenshots to detect visual changes. + +### Test Sharding + +Split tests across parallel processes for faster CI runs. + +### Architecture Testing + +Pest 4 includes architecture testing (from Pest 3): + + +```php +arch('controllers') + ->expect('App\Http\Controllers') + ->toExtendNothing() + ->toHaveSuffix('Controller'); +``` + +## Common Pitfalls + +- Not importing `use function Pest\Laravel\mock;` before using mock +- Using `assertStatus(200)` instead of `assertSuccessful()` +- Forgetting datasets for repetitive validation tests +- Deleting tests without approval +- Forgetting `assertNoJavaScriptErrors()` in browser tests \ No newline at end of file diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 6ee7cad..5adac61 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -15,7 +15,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' coverage: none - name: Install composer dependencies diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6cd71ce..f3840af 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv coverage: none diff --git a/.junie/skills/laravel-best-practices/SKILL.md b/.junie/skills/laravel-best-practices/SKILL.md new file mode 100644 index 0000000..99018f3 --- /dev/null +++ b/.junie/skills/laravel-best-practices/SKILL.md @@ -0,0 +1,190 @@ +--- +name: laravel-best-practices +description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns." +license: MIT +metadata: + author: laravel +--- + +# Laravel Best Practices + +Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`. + +## Consistency First + +Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern. + +Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides. + +## Quick Reference + +### 1. Database Performance → `rules/db-performance.md` + +- Eager load with `with()` to prevent N+1 queries +- Enable `Model::preventLazyLoading()` in development +- Select only needed columns, avoid `SELECT *` +- `chunk()` / `chunkById()` for large datasets +- Index columns used in `WHERE`, `ORDER BY`, `JOIN` +- `withCount()` instead of loading relations to count +- `cursor()` for memory-efficient read-only iteration +- Never query in Blade templates + +### 2. Advanced Query Patterns → `rules/advanced-queries.md` + +- `addSelect()` subqueries over eager-loading entire has-many for a single value +- Dynamic relationships via subquery FK + `belongsTo` +- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries +- `setRelation()` to prevent circular N+1 queries +- `whereIn` + `pluck()` over `whereHas` for better index usage +- Two simple queries can beat one complex query +- Compound indexes matching `orderBy` column order +- Correlated subqueries in `orderBy` for has-many sorting (avoid joins) + +### 3. Security → `rules/security.md` + +- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates +- No raw SQL with user input — use Eloquent or query builder +- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes +- Validate MIME type, extension, and size for file uploads +- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields + +### 4. Caching → `rules/caching.md` + +- `Cache::remember()` over manual get/put +- `Cache::flexible()` for stale-while-revalidate on high-traffic data +- `Cache::memo()` to avoid redundant cache hits within a request +- Cache tags to invalidate related groups +- `Cache::add()` for atomic conditional writes +- `once()` to memoize per-request or per-object lifetime +- `Cache::lock()` / `lockForUpdate()` for race conditions +- Failover cache stores in production + +### 5. Eloquent Patterns → `rules/eloquent.md` + +- Correct relationship types with return type hints +- Local scopes for reusable query constraints +- Global scopes sparingly — document their existence +- Attribute casts in the `casts()` method +- Cast date columns, use Carbon instances in templates +- `whereBelongsTo($model)` for cleaner queries +- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries + +### 6. Validation & Forms → `rules/validation.md` + +- Form Request classes, not inline validation +- Array notation `['required', 'email']` for new code; follow existing convention +- `$request->validated()` only — never `$request->all()` +- `Rule::when()` for conditional validation +- `after()` instead of `withValidator()` + +### 7. Configuration → `rules/config.md` + +- `env()` only inside config files +- `App::environment()` or `app()->isProduction()` +- Config, lang files, and constants over hardcoded text + +### 8. Testing Patterns → `rules/testing.md` + +- `LazilyRefreshDatabase` over `RefreshDatabase` for speed +- `assertModelExists()` over raw `assertDatabaseHas()` +- Factory states and sequences over manual overrides +- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before +- `recycle()` to share relationship instances across factories + +### 9. Queue & Job Patterns → `rules/queue-jobs.md` + +- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]` +- `ShouldBeUnique` to prevent duplicates; `WithoutOverlapping::untilProcessing()` for concurrency +- Always implement `failed()`; with `retryUntil()`, set `$tries = 0` +- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs +- Horizon for complex multi-queue scenarios + +### 10. Routing & Controllers → `rules/routing.md` + +- Implicit route model binding +- Scoped bindings for nested resources +- `Route::resource()` or `apiResource()` +- Methods under 10 lines — extract to actions/services +- Type-hint Form Requests for auto-validation + +### 11. HTTP Client → `rules/http-client.md` + +- Explicit `timeout` and `connectTimeout` on every request +- `retry()` with exponential backoff for external APIs +- Check response status or use `throw()` +- `Http::pool()` for concurrent independent requests +- `Http::fake()` and `preventStrayRequests()` in tests + +### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md` + +- Event discovery over manual registration; `event:cache` in production +- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions +- Queue notifications and mailables with `ShouldQueue` +- On-demand notifications for non-user recipients +- `HasLocalePreference` on notifiable models +- `assertQueued()` not `assertSent()` for queued mailables +- Markdown mailables for transactional emails + +### 13. Error Handling → `rules/error-handling.md` + +- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern +- `ShouldntReport` for exceptions that should never log +- Throttle high-volume exceptions to protect log sinks +- `dontReportDuplicates()` for multi-catch scenarios +- Force JSON rendering for API routes +- Structured context via `context()` on exception classes + +### 14. Task Scheduling → `rules/scheduling.md` + +- `withoutOverlapping()` on variable-duration tasks +- `onOneServer()` on multi-server deployments +- `runInBackground()` for concurrent long tasks +- `environments()` to restrict to appropriate environments +- `takeUntilTimeout()` for time-bounded processing +- Schedule groups for shared configuration + +### 15. Architecture → `rules/architecture.md` + +- Single-purpose Action classes; dependency injection over `app()` helper +- Prefer official Laravel packages and follow conventions, don't override defaults +- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety +- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution + +### 16. Migrations → `rules/migrations.md` + +- Generate migrations with `php artisan make:migration` +- `constrained()` for foreign keys +- Never modify migrations that have run in production +- Add indexes in the migration, not as an afterthought +- Mirror column defaults in model `$attributes` +- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes +- One concern per migration — never mix DDL and DML + +### 17. Collections → `rules/collections.md` + +- Higher-order messages for simple collection operations +- `cursor()` vs. `lazy()` — choose based on relationship needs +- `lazyById()` when updating records while iterating +- `toQuery()` for bulk operations on collections + +### 18. Blade & Views → `rules/blade-views.md` + +- `$attributes->merge()` in component templates +- Blade components over `@include`; `@pushOnce` for per-component scripts +- View Composers for shared view data +- `@aware` for deeply nested component props + +### 19. Conventions & Style → `rules/style.md` + +- Follow Laravel naming conventions for all entities +- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions +- No JS/CSS in Blade, no HTML in PHP classes +- Code should be readable; comments only for config files + +## How to Apply + +Always use a sub-agent to read rule files and explore this skill's content. + +1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10) +2. Check sibling files for existing patterns — follow those first per Consistency First +3. Verify API syntax with `search-docs` for the installed Laravel version \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/advanced-queries.md b/.junie/skills/laravel-best-practices/rules/advanced-queries.md new file mode 100644 index 0000000..920714a --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/advanced-queries.md @@ -0,0 +1,106 @@ +# Advanced Query Patterns + +## Use `addSelect()` Subqueries for Single Values from Has-Many + +Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries. + +```php +public function scopeWithLastLoginAt($query): void +{ + $query->addSelect([ + 'last_login_at' => Login::select('created_at') + ->whereColumn('user_id', 'users.id') + ->latest() + ->take(1), + ])->withCasts(['last_login_at' => 'datetime']); +} +``` + +## Create Dynamic Relationships via Subquery FK + +Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection. + +```php +public function lastLogin(): BelongsTo +{ + return $this->belongsTo(Login::class); +} + +public function scopeWithLastLogin($query): void +{ + $query->addSelect([ + 'last_login_id' => Login::select('id') + ->whereColumn('user_id', 'users.id') + ->latest() + ->take(1), + ])->with('lastLogin'); +} +``` + +## Use Conditional Aggregates Instead of Multiple Count Queries + +Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values. + +```php +$statuses = Feature::toBase() + ->selectRaw("count(case when status = 'Requested' then 1 end) as requested") + ->selectRaw("count(case when status = 'Planned' then 1 end) as planned") + ->selectRaw("count(case when status = 'Completed' then 1 end) as completed") + ->first(); +``` + +## Use `setRelation()` to Prevent Circular N+1 + +When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries. + +```php +$feature->load('comments.user'); +$feature->comments->each->setRelation('feature', $feature); +``` + +## Prefer `whereIn` + Subquery Over `whereHas` + +`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory. + +Incorrect (correlated EXISTS re-executes per row): + +```php +$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term)); +``` + +Correct (index-friendly subquery, no PHP memory overhead): + +```php +$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id')); +``` + +## Sometimes Two Simple Queries Beat One Complex Query + +Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index. + +## Use Compound Indexes Matching `orderBy` Column Order + +When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index. + +```php +// Migration +$table->index(['last_name', 'first_name']); + +// Query — column order must match the index +User::query()->orderBy('last_name')->orderBy('first_name')->paginate(); +``` + +## Use Correlated Subqueries for Has-Many Ordering + +When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading. + +```php +public function scopeOrderByLastLogin($query): void +{ + $query->orderByDesc(Login::select('created_at') + ->whereColumn('user_id', 'users.id') + ->latest() + ->take(1) + ); +} +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/architecture.md b/.junie/skills/laravel-best-practices/rules/architecture.md new file mode 100644 index 0000000..1650564 --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/architecture.md @@ -0,0 +1,202 @@ +# Architecture Best Practices + +## Single-Purpose Action Classes + +Extract discrete business operations into invokable Action classes. + +```php +class CreateOrderAction +{ + public function __construct(private InventoryService $inventory) {} + + public function execute(array $data): Order + { + $order = Order::create($data); + $this->inventory->reserve($order); + + return $order; + } +} +``` + +## Use Dependency Injection + +Always use constructor injection. Avoid `app()` or `resolve()` inside classes. + +Incorrect: +```php +class OrderController extends Controller +{ + public function store(StoreOrderRequest $request) + { + $service = app(OrderService::class); + + return $service->create($request->validated()); + } +} +``` + +Correct: +```php +class OrderController extends Controller +{ + public function __construct(private OrderService $service) {} + + public function store(StoreOrderRequest $request) + { + return $this->service->create($request->validated()); + } +} +``` + +## Code to Interfaces + +Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability. + +Incorrect (concrete dependency): +```php +class OrderService +{ + public function __construct(private StripeGateway $gateway) {} +} +``` + +Correct (interface dependency): +```php +interface PaymentGateway +{ + public function charge(int $amount, string $customerId): PaymentResult; +} + +class OrderService +{ + public function __construct(private PaymentGateway $gateway) {} +} +``` + +Bind in a service provider: + +```php +$this->app->bind(PaymentGateway::class, StripeGateway::class); +``` + +## Default Sort by Descending + +When no explicit order is specified, sort by `id` or `created_at` descending. Explicit ordering prevents cross-database inconsistencies between MySQL and Postgres. + +Incorrect: +```php +$posts = Post::paginate(); +``` + +Correct: +```php +$posts = Post::latest()->paginate(); +``` + +## Use Atomic Locks for Race Conditions + +Prevent race conditions with `Cache::lock()` or `lockForUpdate()`. + +```php +Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) { + $order->process(); +}); + +// Or at query level +$product = Product::where('id', $id)->lockForUpdate()->first(); +``` + +## Use `mb_*` String Functions + +When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters. + +Incorrect: +```php +strlen('José'); // 5 (bytes, not characters) +strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte +``` + +Correct: +```php +mb_strlen('José'); // 4 (characters) +mb_strtolower('MÜNCHEN'); // 'münchen' + +// Prefer Laravel's Str helpers when available +Str::length('José'); // 4 +Str::lower('MÜNCHEN'); // 'münchen' +``` + +## Use `defer()` for Post-Response Work + +For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead. + +Incorrect (job overhead for trivial work): +```php +dispatch(new LogPageView($page)); +``` + +Correct (runs after response, same process): +```php +defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()])); +``` + +Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work. + +## Use `Context` for Request-Scoped Data + +The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually. + +```php +// In middleware +Context::add('tenant_id', $request->header('X-Tenant-ID')); + +// Anywhere later — controllers, jobs, log context +$tenantId = Context::get('tenant_id'); +``` + +Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`. + +## Use `Concurrency::run()` for Parallel Execution + +Run independent operations in parallel using child processes — no async libraries needed. + +```php +use Illuminate\Support\Facades\Concurrency; + +[$users, $orders] = Concurrency::run([ + fn () => User::count(), + fn () => Order::where('status', 'pending')->count(), +]); +``` + +Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially. + +## Convention Over Configuration + +Follow Laravel conventions. Don't override defaults unnecessarily. + +Incorrect: +```php +class Customer extends Model +{ + protected $table = 'Customer'; + protected $primaryKey = 'customer_id'; + + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id'); + } +} +``` + +Correct: +```php +class Customer extends Model +{ + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class); + } +} +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/blade-views.md b/.junie/skills/laravel-best-practices/rules/blade-views.md new file mode 100644 index 0000000..c6f8aaf --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/blade-views.md @@ -0,0 +1,36 @@ +# Blade & Views Best Practices + +## Use `$attributes->merge()` in Component Templates + +Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly. + +```blade +
merge(['class' => 'alert alert-'.$type]) }}> + {{ $message }} +
+``` + +## Use `@pushOnce` for Per-Component Scripts + +If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once. + +## Prefer Blade Components Over `@include` + +`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots. + +## Use View Composers for Shared View Data + +If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it. + +## Use Blade Fragments for Partial Re-Renders (htmx/Turbo) + +A single view can return either the full page or just a fragment, keeping routing clean. + +```php +return view('dashboard', compact('users')) + ->fragmentIf($request->hasHeader('HX-Request'), 'user-list'); +``` + +## Use `@aware` for Deeply Nested Component Props + +Avoids re-passing parent props through every level of nested components. \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/caching.md b/.junie/skills/laravel-best-practices/rules/caching.md new file mode 100644 index 0000000..eb3ef3e --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/caching.md @@ -0,0 +1,70 @@ +# Caching Best Practices + +## Use `Cache::remember()` Instead of Manual Get/Put + +Atomic pattern prevents race conditions and removes boilerplate. + +Incorrect: +```php +$val = Cache::get('stats'); +if (! $val) { + $val = $this->computeStats(); + Cache::put('stats', $val, 60); +} +``` + +Correct: +```php +$val = Cache::remember('stats', 60, fn () => $this->computeStats()); +``` + +## Use `Cache::flexible()` for Stale-While-Revalidate + +On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background. + +Incorrect: `Cache::remember('users', 300, fn () => User::all());` + +Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function. + +## Use `Cache::memo()` to Avoid Redundant Hits Within a Request + +If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory. + +`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5. + +## Use Cache Tags to Invalidate Related Groups + +Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`. + +```php +Cache::tags(['user-1'])->flush(); +``` + +## Use `Cache::add()` for Atomic Conditional Writes + +`add()` only writes if the key does not exist — atomic, no race condition between checking and writing. + +Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }` + +Correct: `Cache::add('lock', true, 10);` + +## Use `once()` for Per-Request Memoization + +`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory. + +```php +public function roles(): Collection +{ + return once(fn () => $this->loadRoles()); +} +``` + +Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching. + +## Configure Failover Cache Stores in Production + +If Redis goes down, the app falls back to a secondary store automatically. + +```php +'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']], +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/collections.md b/.junie/skills/laravel-best-practices/rules/collections.md new file mode 100644 index 0000000..14f683d --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/collections.md @@ -0,0 +1,44 @@ +# Collection Best Practices + +## Use Higher-Order Messages for Simple Operations + +Incorrect: +```php +$users->each(function (User $user) { + $user->markAsVip(); +}); +``` + +Correct: `$users->each->markAsVip();` + +Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc. + +## Choose `cursor()` vs. `lazy()` Correctly + +- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk). +- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading. + +Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored. + +Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work. + +## Use `lazyById()` When Updating Records While Iterating + +`lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation. + +## Use `toQuery()` for Bulk Operations on Collections + +Avoids manual `whereIn` construction. + +Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);` + +Correct: `$users->toQuery()->update([...]);` + +## Use `#[CollectedBy]` for Custom Collection Classes + +More declarative than overriding `newCollection()`. + +```php +#[CollectedBy(UserCollection::class)] +class User extends Model {} +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/config.md b/.junie/skills/laravel-best-practices/rules/config.md new file mode 100644 index 0000000..8fd8f53 --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/config.md @@ -0,0 +1,73 @@ +# Configuration Best Practices + +## `env()` Only in Config Files + +Direct `env()` calls return `null` when config is cached. + +Incorrect: +```php +$key = env('API_KEY'); +``` + +Correct: +```php +// config/services.php +'key' => env('API_KEY'), + +// Application code +$key = config('services.key'); +``` + +## Use Encrypted Env or External Secrets + +Never store production secrets in plain `.env` files in version control. + +Incorrect: +```bash + +# .env committed to repo or shared in Slack + +STRIPE_SECRET=sk_live_abc123 +AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI +``` + +Correct: +```bash +php artisan env:encrypt --env=production --readable +php artisan env:decrypt --env=production +``` + +For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime. + +## Use `App::environment()` for Environment Checks + +Incorrect: +```php +if (env('APP_ENV') === 'production') { +``` + +Correct: +```php +if (app()->isProduction()) { +// or +if (App::environment('production')) { +``` + +## Use Constants and Language Files + +Use class constants instead of hardcoded magic strings for model states, types, and statuses. + +```php +// Incorrect +return $this->type === 'normal'; + +// Correct +return $this->type === self::TYPE_NORMAL; +``` + +If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there. + +```php +// Only when lang files already exist in the project +return back()->with('message', __('app.article_added')); +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/db-performance.md b/.junie/skills/laravel-best-practices/rules/db-performance.md new file mode 100644 index 0000000..8fb7193 --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/db-performance.md @@ -0,0 +1,192 @@ +# Database Performance Best Practices + +## Always Eager Load Relationships + +Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront. + +Incorrect (N+1 — executes 1 + N queries): +```php +$posts = Post::all(); +foreach ($posts as $post) { + echo $post->author->name; +} +``` + +Correct (2 queries total): +```php +$posts = Post::with('author')->get(); +foreach ($posts as $post) { + echo $post->author->name; +} +``` + +Constrain eager loads to select only needed columns (always include the foreign key): + +```php +$users = User::with(['posts' => function ($query) { + $query->select('id', 'user_id', 'title') + ->where('published', true) + ->latest() + ->limit(10); +}])->get(); +``` + +## Prevent Lazy Loading in Development + +Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development. + +```php +public function boot(): void +{ + Model::preventLazyLoading(! app()->isProduction()); +} +``` + +Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded. + +## Select Only Needed Columns + +Avoid `SELECT *` — especially when tables have large text or JSON columns. + +Incorrect: +```php +$posts = Post::with('author')->get(); +``` + +Correct: +```php +$posts = Post::select('id', 'title', 'user_id', 'created_at') + ->with(['author:id,name,avatar']) + ->get(); +``` + +When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match. + +## Chunk Large Datasets + +Never load thousands of records at once. Use chunking for batch processing. + +Incorrect: +```php +$users = User::all(); +foreach ($users as $user) { + $user->notify(new WeeklyDigest); +} +``` + +Correct: +```php +User::where('subscribed', true)->chunk(200, function ($users) { + foreach ($users as $user) { + $user->notify(new WeeklyDigest); + } +}); +``` + +Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change: + +```php +User::where('active', false)->chunkById(200, function ($users) { + $users->each->delete(); +}); +``` + +## Add Database Indexes + +Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses. + +Incorrect: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained(); + $table->string('status'); + $table->timestamps(); +}); +``` + +Correct: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->index()->constrained(); + $table->string('status')->index(); + $table->timestamps(); + $table->index(['status', 'created_at']); +}); +``` + +Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`). + +## Use `withCount()` for Counting Relations + +Never load entire collections just to count them. + +Incorrect: +```php +$posts = Post::all(); +foreach ($posts as $post) { + echo $post->comments->count(); +} +``` + +Correct: +```php +$posts = Post::withCount('comments')->get(); +foreach ($posts as $post) { + echo $post->comments_count; +} +``` + +Conditional counting: + +```php +$posts = Post::withCount([ + 'comments', + 'comments as approved_comments_count' => function ($query) { + $query->where('approved', true); + }, +])->get(); +``` + +## Use `cursor()` for Memory-Efficient Iteration + +For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator. + +Incorrect: +```php +$users = User::where('active', true)->get(); +``` + +Correct: +```php +foreach (User::where('active', true)->cursor() as $user) { + ProcessUser::dispatch($user->id); +} +``` + +Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records. + +## No Queries in Blade Templates + +Never execute queries in Blade templates. Pass data from controllers. + +Incorrect: +```blade +@foreach (User::all() as $user) + {{ $user->profile->name }} +@endforeach +``` + +Correct: +```php +// Controller +$users = User::with('profile')->get(); +return view('users.index', compact('users')); +``` + +```blade +@foreach ($users as $user) + {{ $user->profile->name }} +@endforeach +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/eloquent.md b/.junie/skills/laravel-best-practices/rules/eloquent.md new file mode 100644 index 0000000..09cd66a --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/eloquent.md @@ -0,0 +1,148 @@ +# Eloquent Best Practices + +## Use Correct Relationship Types + +Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints. + +```php +public function comments(): HasMany +{ + return $this->hasMany(Comment::class); +} + +public function author(): BelongsTo +{ + return $this->belongsTo(User::class, 'user_id'); +} +``` + +## Use Local Scopes for Reusable Queries + +Extract reusable query constraints into local scopes to avoid duplication. + +Incorrect: +```php +$active = User::where('verified', true)->whereNotNull('activated_at')->get(); +$articles = Article::whereHas('user', function ($q) { + $q->where('verified', true)->whereNotNull('activated_at'); +})->get(); +``` + +Correct: +```php +public function scopeActive(Builder $query): Builder +{ + return $query->where('verified', true)->whereNotNull('activated_at'); +} + +// Usage +$active = User::active()->get(); +$articles = Article::whereHas('user', fn ($q) => $q->active())->get(); +``` + +## Apply Global Scopes Sparingly + +Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy. + +Incorrect (global scope for a conditional filter): +```php +class PublishedScope implements Scope +{ + public function apply(Builder $builder, Model $model): void + { + $builder->where('published', true); + } +} +// Now admin panels, reports, and background jobs all silently skip drafts +``` + +Correct (local scope you opt into): +```php +public function scopePublished(Builder $query): Builder +{ + return $query->where('published', true); +} + +Post::published()->paginate(); // Explicit +Post::paginate(); // Admin sees all +``` + +## Define Attribute Casts + +Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion. + +```php +protected function casts(): array +{ + return [ + 'is_active' => 'boolean', + 'metadata' => 'array', + 'total' => 'decimal:2', + ]; +} +``` + +## Cast Date Columns Properly + +Always cast date columns. Use Carbon instances in templates instead of formatting strings manually. + +Incorrect: +```blade +{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }} +``` + +Correct: +```php +protected function casts(): array +{ + return [ + 'ordered_at' => 'datetime', + ]; +} +``` + +```blade +{{ $order->ordered_at->toDateString() }} +{{ $order->ordered_at->format('m-d') }} +``` + +## Use `whereBelongsTo()` for Relationship Queries + +Cleaner than manually specifying foreign keys. + +Incorrect: +```php +Post::where('user_id', $user->id)->get(); +``` + +Correct: +```php +Post::whereBelongsTo($user)->get(); +Post::whereBelongsTo($user, 'author')->get(); +``` + +## Avoid Hardcoded Table Names in Queries + +Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string). + +Incorrect: +```php +DB::table('users')->where('active', true)->get(); + +$query->join('companies', 'companies.id', '=', 'users.company_id'); + +DB::select('SELECT * FROM orders WHERE status = ?', ['pending']); +``` + +Correct — reference the model's table: +```php +DB::table((new User)->getTable())->where('active', true)->get(); + +// Even better — use Eloquent or the query builder instead of raw SQL +User::where('active', true)->get(); +Order::where('status', 'pending')->get(); +``` + +Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable. + +**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration. \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/error-handling.md b/.junie/skills/laravel-best-practices/rules/error-handling.md new file mode 100644 index 0000000..bb8e7a3 --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/error-handling.md @@ -0,0 +1,72 @@ +# Error Handling Best Practices + +## Exception Reporting and Rendering + +There are two valid approaches — choose one and apply it consistently across the project. + +**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find: + +```php +class InvalidOrderException extends Exception +{ + public function report(): void { /* custom reporting */ } + + public function render(Request $request): Response + { + return response()->view('errors.invalid-order', status: 422); + } +} +``` + +**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture: + +```php +->withExceptions(function (Exceptions $exceptions) { + $exceptions->report(function (InvalidOrderException $e) { /* ... */ }); + $exceptions->render(function (InvalidOrderException $e, Request $request) { + return response()->view('errors.invalid-order', status: 422); + }); +}) +``` + +Check the existing codebase and follow whichever pattern is already established. + +## Use `ShouldntReport` for Exceptions That Should Never Log + +More discoverable than listing classes in `dontReport()`. + +```php +class PodcastProcessingException extends Exception implements ShouldntReport {} +``` + +## Throttle High-Volume Exceptions + +A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type. + +## Enable `dontReportDuplicates()` + +Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks. + +## Force JSON Error Rendering for API Routes + +Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes. + +```php +$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { + return $request->is('api/*') || $request->expectsJson(); +}); +``` + +## Add Context to Exception Classes + +Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry. + +```php +class InvalidOrderException extends Exception +{ + public function context(): array + { + return ['order_id' => $this->orderId]; + } +} +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/events-notifications.md b/.junie/skills/laravel-best-practices/rules/events-notifications.md new file mode 100644 index 0000000..bc43f19 --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/events-notifications.md @@ -0,0 +1,48 @@ +# Events & Notifications Best Practices + +## Rely on Event Discovery + +Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`. + +## Run `event:cache` in Production Deploy + +Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`. + +## Use `ShouldDispatchAfterCommit` Inside Transactions + +Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet. + +```php +class OrderShipped implements ShouldDispatchAfterCommit {} +``` + +## Always Queue Notifications + +Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response. + +```php +class InvoicePaid extends Notification implements ShouldQueue +{ + use Queueable; +} +``` + +## Use `afterCommit()` on Notifications in Transactions + +Same race condition as events — the queued notification job may run before the transaction commits. + +## Route Notification Channels to Dedicated Queues + +Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues. + +## Use On-Demand Notifications for Non-User Recipients + +Avoid creating dummy models to send notifications to arbitrary addresses. + +```php +Notification::route('mail', 'admin@example.com')->notify(new SystemAlert()); +``` + +## Implement `HasLocalePreference` on Notifiable Models + +Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed. \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/http-client.md b/.junie/skills/laravel-best-practices/rules/http-client.md new file mode 100644 index 0000000..0a7876e --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/http-client.md @@ -0,0 +1,160 @@ +# HTTP Client Best Practices + +## Always Set Explicit Timeouts + +The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast. + +Incorrect: +```php +$response = Http::get('https://api.example.com/users'); +``` + +Correct: +```php +$response = Http::timeout(5) + ->connectTimeout(3) + ->get('https://api.example.com/users'); +``` + +For service-specific clients, define timeouts in a macro: + +```php +Http::macro('github', function () { + return Http::baseUrl('https://api.github.com') + ->timeout(10) + ->connectTimeout(3) + ->withToken(config('services.github.token')); +}); + +$response = Http::github()->get('/repos/laravel/framework'); +``` + +## Use Retry with Backoff for External APIs + +External APIs have transient failures. Use `retry()` with increasing delays. + +Incorrect: +```php +$response = Http::post('https://api.stripe.com/v1/charges', $data); + +if ($response->failed()) { + throw new PaymentFailedException('Charge failed'); +} +``` + +Correct: +```php +$response = Http::retry([100, 500, 1000]) + ->timeout(10) + ->post('https://api.stripe.com/v1/charges', $data); +``` + +Only retry on specific errors: + +```php +$response = Http::retry(3, 100, function (Exception $exception, PendingRequest $request) { + return $exception instanceof ConnectionException + || ($exception instanceof RequestException && $exception->response->serverError()); +})->post('https://api.example.com/data'); +``` + +## Handle Errors Explicitly + +The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`. + +Incorrect: +```php +$response = Http::get('https://api.example.com/users/1'); +$user = $response->json(); // Could be an error body +``` + +Correct: +```php +$response = Http::timeout(5) + ->get('https://api.example.com/users/1') + ->throw(); + +$user = $response->json(); +``` + +For graceful degradation: + +```php +$response = Http::get('https://api.example.com/users/1'); + +if ($response->successful()) { + return $response->json(); +} + +if ($response->notFound()) { + return null; +} + +$response->throw(); +``` + +## Use Request Pooling for Concurrent Requests + +When making multiple independent API calls, use `Http::pool()` instead of sequential calls. + +Incorrect: +```php +$users = Http::get('https://api.example.com/users')->json(); +$posts = Http::get('https://api.example.com/posts')->json(); +$comments = Http::get('https://api.example.com/comments')->json(); +``` + +Correct: +```php +use Illuminate\Http\Client\Pool; + +$responses = Http::pool(fn (Pool $pool) => [ + $pool->as('users')->get('https://api.example.com/users'), + $pool->as('posts')->get('https://api.example.com/posts'), + $pool->as('comments')->get('https://api.example.com/comments'), +]); + +$users = $responses['users']->json(); +$posts = $responses['posts']->json(); +``` + +## Fake HTTP Calls in Tests + +Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`. + +Incorrect: +```php +it('syncs user from API', function () { + $service = new UserSyncService; + $service->sync(1); // Hits the real API +}); +``` + +Correct: +```php +it('syncs user from API', function () { + Http::preventStrayRequests(); + + Http::fake([ + 'api.example.com/users/1' => Http::response([ + 'name' => 'John Doe', + 'email' => 'john@example.com', + ]), + ]); + + $service = new UserSyncService; + $service->sync(1); + + Http::assertSent(function (Request $request) { + return $request->url() === 'https://api.example.com/users/1'; + }); +}); +``` + +Test failure scenarios too: + +```php +Http::fake([ + 'api.example.com/*' => Http::failedConnection(), +]); +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/mail.md b/.junie/skills/laravel-best-practices/rules/mail.md new file mode 100644 index 0000000..c7f6796 --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/mail.md @@ -0,0 +1,27 @@ +# Mail Best Practices + +## Implement `ShouldQueue` on the Mailable Class + +Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it. + +## Use `afterCommit()` on Mailables Inside Transactions + +A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor. + +## Use `assertQueued()` Not `assertSent()` for Queued Mailables + +`Mail::assertSent()` only catches synchronous mail. Queued mailables silently pass `assertSent`, giving false confidence. + +Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`. + +Correct: `Mail::assertQueued(OrderShipped::class);` + +## Use Markdown Mailables for Transactional Emails + +Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag. + +## Separate Content Tests from Sending Tests + +Content tests: instantiate the mailable directly, call `assertSeeInHtml()`. +Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`. +Don't mix them — it conflates concerns and makes tests brittle. \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/migrations.md b/.junie/skills/laravel-best-practices/rules/migrations.md new file mode 100644 index 0000000..de25aa3 --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/migrations.md @@ -0,0 +1,121 @@ +# Migration Best Practices + +## Generate Migrations with Artisan + +Always use `php artisan make:migration` for consistent naming and timestamps. + +Incorrect (manually created file): +```php +// database/migrations/posts_migration.php ← wrong naming, no timestamp +``` + +Correct (Artisan-generated): +```bash +php artisan make:migration create_posts_table +php artisan make:migration add_slug_to_posts_table +``` + +## Use `constrained()` for Foreign Keys + +Automatic naming and referential integrity. + +```php +$table->foreignId('user_id')->constrained()->cascadeOnDelete(); + +// Non-standard names +$table->foreignId('author_id')->constrained('users'); +``` + +## Never Modify Deployed Migrations + +Once a migration has run in production, treat it as immutable. Create a new migration to change the table. + +Incorrect (editing a deployed migration): +```php +// 2024_01_01_create_posts_table.php — already in production +$table->string('slug')->unique(); // ← added after deployment +``` + +Correct (new migration to alter): +```php +// 2024_03_15_add_slug_to_posts_table.php +Schema::table('posts', function (Blueprint $table) { + $table->string('slug')->unique()->after('title'); +}); +``` + +## Add Indexes in the Migration + +Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes. + +Incorrect: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained(); + $table->string('status'); + $table->timestamps(); +}); +``` + +Correct: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained()->index(); + $table->string('status')->index(); + $table->timestamp('shipped_at')->nullable()->index(); + $table->timestamps(); +}); +``` + +## Mirror Defaults in Model `$attributes` + +When a column has a database default, mirror it in the model so new instances have correct values before saving. + +```php +// Migration +$table->string('status')->default('pending'); + +// Model +protected $attributes = [ + 'status' => 'pending', +]; +``` + +## Write Reversible `down()` Methods by Default + +Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments. + +```php +public function down(): void +{ + Schema::table('posts', function (Blueprint $table) { + $table->dropColumn('slug'); + }); +} +``` + +For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported. + +## Keep Migrations Focused + +One concern per migration. Never mix DDL (schema changes) and DML (data manipulation). + +Incorrect (partial failure creates unrecoverable state): +```php +public function up(): void +{ + Schema::create('settings', function (Blueprint $table) { ... }); + DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']); +} +``` + +Correct (separate migrations): +```php +// Migration 1: create_settings_table +Schema::create('settings', function (Blueprint $table) { ... }); + +// Migration 2: seed_default_settings +DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']); +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/queue-jobs.md b/.junie/skills/laravel-best-practices/rules/queue-jobs.md new file mode 100644 index 0000000..d4575aa --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/queue-jobs.md @@ -0,0 +1,146 @@ +# Queue & Job Best Practices + +## Set `retry_after` Greater Than `timeout` + +If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution. + +Incorrect (`retry_after` ≤ `timeout`): +```php +class ProcessReport implements ShouldQueue +{ + public $timeout = 120; +} + +// config/queue.php — retry_after: 90 ← job retried while still running! +``` + +Correct (`retry_after` > `timeout`): +```php +class ProcessReport implements ShouldQueue +{ + public $timeout = 120; +} + +// config/queue.php — retry_after: 180 ← safely longer than any job timeout +``` + +## Use Exponential Backoff + +Use progressively longer delays between retries to avoid hammering failing services. + +Incorrect (fixed retry interval): +```php +class SyncWithStripe implements ShouldQueue +{ + public $tries = 3; + // Default: retries immediately, overwhelming the API +} +``` + +Correct (exponential backoff): +```php +class SyncWithStripe implements ShouldQueue +{ + public $tries = 3; + public $backoff = [1, 5, 10]; +} +``` + +## Implement `ShouldBeUnique` + +Prevent duplicate job processing. + +```php +class GenerateInvoice implements ShouldQueue, ShouldBeUnique +{ + public function uniqueId(): string + { + return $this->order->id; + } + + public $uniqueFor = 3600; +} +``` + +## Always Implement `failed()` + +Handle errors explicitly — don't rely on silent failure. + +```php +public function failed(?Throwable $exception): void +{ + $this->podcast->update(['status' => 'failed']); + Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]); +} +``` + +## Rate Limit External API Calls in Jobs + +Use `RateLimited` middleware to throttle jobs calling third-party APIs. + +```php +public function middleware(): array +{ + return [new RateLimited('external-api')]; +} +``` + +## Batch Related Jobs + +Use `Bus::batch()` when jobs should succeed or fail together. + +```php +Bus::batch([ + new ImportCsvChunk($chunk1), + new ImportCsvChunk($chunk2), +]) +->then(fn (Batch $batch) => Notification::send($user, new ImportComplete)) +->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed')) +->dispatch(); +``` + +## `retryUntil()` Needs `$tries = 0` + +When using time-based retry limits, set `$tries = 0` to avoid premature failure. + +```php +public $tries = 0; + +public function retryUntil(): DateTime +{ + return now()->addHours(4); +} +``` + +## Use `WithoutOverlapping::untilProcessing()` + +Prevents concurrent execution while allowing new instances to queue. + +```php +public function middleware(): array +{ + return [new WithoutOverlapping($this->product->id)->untilProcessing()]; +} +``` + +Without `untilProcessing()`, the lock extends through queue wait time. With it, the lock releases when processing starts. + +## Use Horizon for Complex Queue Scenarios + +Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities. + +```php +// config/horizon.php +'environments' => [ + 'production' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['high', 'default', 'low'], + 'balance' => 'auto', + 'minProcesses' => 1, + 'maxProcesses' => 10, + 'tries' => 3, + ], + ], +], +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/routing.md b/.junie/skills/laravel-best-practices/rules/routing.md new file mode 100644 index 0000000..e288375 --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/routing.md @@ -0,0 +1,98 @@ +# Routing & Controllers Best Practices + +## Use Implicit Route Model Binding + +Let Laravel resolve models automatically from route parameters. + +Incorrect: +```php +public function show(int $id) +{ + $post = Post::findOrFail($id); +} +``` + +Correct: +```php +public function show(Post $post) +{ + return view('posts.show', ['post' => $post]); +} +``` + +## Use Scoped Bindings for Nested Resources + +Enforce parent-child relationships automatically. + +```php +Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) { + // $post is automatically scoped to $user +})->scopeBindings(); +``` + +## Use Resource Controllers + +Use `Route::resource()` or `apiResource()` for RESTful endpoints. + +```php +Route::resource('posts', PostController::class); +Route::apiResource('api/posts', Api\PostController::class); +``` + +## Keep Controllers Thin + +Aim for under 10 lines per method. Extract business logic to action or service classes. + +Incorrect: +```php +public function store(Request $request) +{ + $validated = $request->validate([...]); + if ($request->hasFile('image')) { + $request->file('image')->move(public_path('images')); + } + $post = Post::create($validated); + $post->tags()->sync($validated['tags']); + event(new PostCreated($post)); + return redirect()->route('posts.show', $post); +} +``` + +Correct: +```php +public function store(StorePostRequest $request, CreatePostAction $create) +{ + $post = $create->execute($request->validated()); + + return redirect()->route('posts.show', $post); +} +``` + +## Type-Hint Form Requests + +Type-hinting Form Requests triggers automatic validation and authorization before the method executes. + +Incorrect: +```php +public function store(Request $request): RedirectResponse +{ + $validated = $request->validate([ + 'title' => ['required', 'max:255'], + 'body' => ['required'], + ]); + + Post::create($validated); + + return redirect()->route('posts.index'); +} +``` + +Correct: +```php +public function store(StorePostRequest $request): RedirectResponse +{ + Post::create($request->validated()); + + return redirect()->route('posts.index'); +} +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/scheduling.md b/.junie/skills/laravel-best-practices/rules/scheduling.md new file mode 100644 index 0000000..dfaefa2 --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/scheduling.md @@ -0,0 +1,39 @@ +# Task Scheduling Best Practices + +## Use `withoutOverlapping()` on Variable-Duration Tasks + +Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion. + +## Use `onOneServer()` on Multi-Server Deployments + +Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached). + +## Use `runInBackground()` for Concurrent Long Tasks + +By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes. + +## Use `environments()` to Restrict Tasks + +Prevent accidental execution of production-only tasks (billing, reporting) on staging. + +```php +Schedule::command('billing:charge')->monthly()->environments(['production']); +``` + +## Use `takeUntilTimeout()` for Time-Bounded Processing + +A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time. + +## Use Schedule Groups for Shared Configuration + +Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks. + +```php +Schedule::daily() + ->onOneServer() + ->timezone('America/New_York') + ->group(function () { + Schedule::command('emails:send --force'); + Schedule::command('emails:prune'); + }); +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/security.md b/.junie/skills/laravel-best-practices/rules/security.md new file mode 100644 index 0000000..524d47e --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/security.md @@ -0,0 +1,198 @@ +# Security Best Practices + +## Mass Assignment Protection + +Every model must define `$fillable` (whitelist) or `$guarded` (blacklist). + +Incorrect: +```php +class User extends Model +{ + protected $guarded = []; // All fields are mass assignable +} +``` + +Correct: +```php +class User extends Model +{ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; +} +``` + +Never use `$guarded = []` on models that accept user input. + +## Authorize Every Action + +Use policies or gates in controllers. Never skip authorization. + +Incorrect: +```php +public function update(Request $request, Post $post) +{ + $post->update($request->validated()); +} +``` + +Correct: +```php +public function update(UpdatePostRequest $request, Post $post) +{ + Gate::authorize('update', $post); + + $post->update($request->validated()); +} +``` + +Or via Form Request: + +```php +public function authorize(): bool +{ + return $this->user()->can('update', $this->route('post')); +} +``` + +## Prevent SQL Injection + +Always use parameter binding. Never interpolate user input into queries. + +Incorrect: +```php +DB::select("SELECT * FROM users WHERE name = '{$request->name}'"); +``` + +Correct: +```php +User::where('name', $request->name)->get(); + +// Raw expressions with bindings +User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get(); +``` + +## Escape Output to Prevent XSS + +Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content. + +Incorrect: +```blade +{!! $user->bio !!} +``` + +Correct: +```blade +{{ $user->bio }} +``` + +## CSRF Protection + +Include `@csrf` in all POST/PUT/DELETE Blade forms. Not needed in Inertia. + +Incorrect: +```blade +
+ +
+``` + +Correct: +```blade +
+ @csrf + +
+``` + +## Rate Limit Auth and API Routes + +Apply `throttle` middleware to authentication and API routes. + +```php +RateLimiter::for('login', function (Request $request) { + return Limit::perMinute(5)->by($request->ip()); +}); + +Route::post('/login', LoginController::class)->middleware('throttle:login'); +``` + +## Validate File Uploads + +Validate MIME type, extension, and size. Never trust client-provided filenames. + +```php +public function rules(): array +{ + return [ + 'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'], + ]; +} +``` + +Store with generated filenames: + +```php +$path = $request->file('avatar')->store('avatars', 'public'); +``` + +## Keep Secrets Out of Code + +Never commit `.env`. Access secrets via `config()` only. + +Incorrect: +```php +$key = env('API_KEY'); +``` + +Correct: +```php +// config/services.php +'api_key' => env('API_KEY'), + +// In application code +$key = config('services.api_key'); +``` + +## Audit Dependencies + +Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment. + +```bash +composer audit +``` + +## Encrypt Sensitive Database Fields + +Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`. + +Incorrect: +```php +class Integration extends Model +{ + protected function casts(): array + { + return [ + 'api_key' => 'string', + ]; + } +} +``` + +Correct: +```php +class Integration extends Model +{ + protected $hidden = ['api_key', 'api_secret']; + + protected function casts(): array + { + return [ + 'api_key' => 'encrypted', + 'api_secret' => 'encrypted', + ]; + } +} +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/style.md b/.junie/skills/laravel-best-practices/rules/style.md new file mode 100644 index 0000000000000000000000000000000000000000..db689bf774d1763ac3ec520a3874015f5421ff5b GIT binary patch literal 4443 zcmb7H|8g6*5$@mj6gxJXBU_TPGV!!SM{H%Ls*_r-YD&%@j)w&AKoK^0I0HCRY@C@s zM4zxv(rqc3Rg4_YS46<&Z-l2$9V!-oHYUfv=K_C|PowZt|LcB75;$1eTUe78Vh&a+E%-f!B^_a3OyuOh!1j+3CF~xh&C6Q z*=`XQmT8HzBMo|P)XsSFwYJu8q05a}Nq8>Un^s}fxWXTc+D!ClW^}bJz?`D4gwP={HL5A^SnD#A?)(C5LRZ zR@zG|^YKcHT#n048H9Q7>X){{QHr&?hq_KiVE^8jd(B0!WswWps*3d4DH&@1R8(75 z(o_>v@X2ovWz1l+2v>~J<^>HjOLin4++uISp>Q7sc= zww6}<$`&|bt}L=XnXE+ip&z}g_gV`3HWN5EPEweC&DDJI?qyj{CR+efKb>jeTy0sD zWtYI5qv?KwV!+7*dZa^2FYxC)TK@TN*ocD0=F&btK-5a%Wxf!e#ktaJd!wnwhV#M0 z|0*OpGDbs1S7xm&uSe55UhMTw=n7u9VGYd&_0x8mxwqVDzMxBM#erT(T>>Yi70@@Er6gkS%swHiLEM*)?2zc&R!b$)u{^0DPCWn-5getf^ zqwL-7)#&%+#9FdML00VP=EV)It0I7c8`GuUi-V&wR=MBE?KnxIkV45w0(RotSq(2 z5JBRcjqs-zn!;f43?h8rSf*Nmx8L*f!4K&P%HqkB0gWjgkH;zaLPU;y;I-Mt_EVJK z5225`U*USQfgjQV7uBs9|qK z8N(d$vlHhUP>F9>t{hc! zpk(1NTthYH##aU%kYRXf1Z5TJ#a|1EE=6q<>`nN5$zBCL1$xV1li$&!Wm{EWjJgF+mOY z2m}FlAue$xtpY2C90Ue9gpB9NU51F{eEN|$BM6k3O;JN_tnZ3KW*Cu$J)pVA7jKfx zaAd+LQR$pk$3cm3G=+DZ*%xEtGUz;w>gE-uon7-1V<>b8zw_u7Tofqy@TeZsE$W5A zjUuG+Q%gDQht~?1jCf*ZqtdaFudezoDD5U(J5b>JS*l%5P?3c6WmL#(LhH_DBsfbQ>Dd4CXi F{|%Ge#T5Vm literal 0 HcmV?d00001 diff --git a/.junie/skills/laravel-best-practices/rules/testing.md b/.junie/skills/laravel-best-practices/rules/testing.md new file mode 100644 index 0000000..d39cc3e --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/testing.md @@ -0,0 +1,43 @@ +# Testing Best Practices + +## Use `LazilyRefreshDatabase` Over `RefreshDatabase` + +`RefreshDatabase` runs all migrations every test run even when the schema hasn't changed. `LazilyRefreshDatabase` only migrates when needed, significantly speeding up large suites. + +## Use Model Assertions Over Raw Database Assertions + +Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);` + +Correct: `$this->assertModelExists($user);` + +More expressive, type-safe, and fails with clearer messages. + +## Use Factory States and Sequences + +Named states make tests self-documenting. Sequences eliminate repetitive setup. + +Incorrect: `User::factory()->create(['email_verified_at' => null]);` + +Correct: `User::factory()->unverified()->create();` + +## Use `Exceptions::fake()` to Assert Exception Reporting + +Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally. + +## Call `Event::fake()` After Factory Setup + +Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models. + +Incorrect: `Event::fake(); $user = User::factory()->create();` + +Correct: `$user = User::factory()->create(); Event::fake();` + +## Use `recycle()` to Share Relationship Instances Across Factories + +Without `recycle()`, nested factories create separate instances of the same conceptual entity. + +```php +Ticket::factory() + ->recycle(Airline::factory()->create()) + ->create(); +``` \ No newline at end of file diff --git a/.junie/skills/laravel-best-practices/rules/validation.md b/.junie/skills/laravel-best-practices/rules/validation.md new file mode 100644 index 0000000..a20202f --- /dev/null +++ b/.junie/skills/laravel-best-practices/rules/validation.md @@ -0,0 +1,75 @@ +# Validation & Forms Best Practices + +## Use Form Request Classes + +Extract validation from controllers into dedicated Form Request classes. + +Incorrect: +```php +public function store(Request $request) +{ + $request->validate([ + 'title' => 'required|max:255', + 'body' => 'required', + ]); +} +``` + +Correct: +```php +public function store(StorePostRequest $request) +{ + Post::create($request->validated()); +} +``` + +## Array vs. String Notation for Rules + +Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses. + +```php +// Preferred for new code +'email' => ['required', 'email', Rule::unique('users')], + +// Follow existing convention if the project uses string notation +'email' => 'required|email|unique:users', +``` + +## Always Use `validated()` + +Get only validated data. Never use `$request->all()` for mass operations. + +Incorrect: +```php +Post::create($request->all()); +``` + +Correct: +```php +Post::create($request->validated()); +``` + +## Use `Rule::when()` for Conditional Validation + +```php +'company_name' => [ + Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']), +], +``` + +## Use the `after()` Method for Custom Validation + +Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields. + +```php +public function after(): array +{ + return [ + function (Validator $validator) { + if ($this->quantity > Product::find($this->product_id)?->stock) { + $validator->errors()->add('quantity', 'Not enough stock.'); + } + }, + ]; +} +``` \ No newline at end of file diff --git a/.junie/skills/pest-testing/SKILL.md b/.junie/skills/pest-testing/SKILL.md new file mode 100644 index 0000000..ba774e7 --- /dev/null +++ b/.junie/skills/pest-testing/SKILL.md @@ -0,0 +1,157 @@ +--- +name: pest-testing +description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code." +license: MIT +metadata: + author: laravel +--- + +# Pest Testing 4 + +## Documentation + +Use `search-docs` for detailed Pest 4 patterns and documentation. + +## Basic Usage + +### Creating Tests + +All tests must be written using Pest. Use `php artisan make:test --pest {name}`. + +### Test Organization + +- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories. +- Browser tests: `tests/Browser/` directory. +- Do NOT remove tests without approval - these are core application code. + +### Basic Test Structure + + +```php +it('is true', function () { + expect(true)->toBeTrue(); +}); +``` + +### Running Tests + +- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`. +- Run all tests: `php artisan test --compact`. +- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`. + +## Assertions + +Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`: + + +```php +it('returns all', function () { + $this->postJson('/api/docs', [])->assertSuccessful(); +}); +``` + +| Use | Instead of | +|-----|------------| +| `assertSuccessful()` | `assertStatus(200)` | +| `assertNotFound()` | `assertStatus(404)` | +| `assertForbidden()` | `assertStatus(403)` | + +## Mocking + +Import mock function before use: `use function Pest\Laravel\mock;` + +## Datasets + +Use datasets for repetitive tests (validation rules, etc.): + + +```php +it('has emails', function (string $email) { + expect($email)->not->toBeEmpty(); +})->with([ + 'james' => 'james@laravel.com', + 'taylor' => 'taylor@laravel.com', +]); +``` + +## Pest 4 Features + +| Feature | Purpose | +|---------|---------| +| Browser Testing | Full integration tests in real browsers | +| Smoke Testing | Validate multiple pages quickly | +| Visual Regression | Compare screenshots for visual changes | +| Test Sharding | Parallel CI runs | +| Architecture Testing | Enforce code conventions | + +### Browser Test Example + +Browser tests run in real browsers for full integration testing: + +- Browser tests live in `tests/Browser/`. +- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories. +- Use `RefreshDatabase` for clean state per test. +- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures. +- Test on multiple browsers (Chrome, Firefox, Safari) if requested. +- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested. +- Switch color schemes (light/dark mode) when appropriate. +- Take screenshots or pause tests for debugging. + + +```php +it('may reset the password', function () { + Notification::fake(); + + $this->actingAs(User::factory()->create()); + + $page = visit('/sign-in'); + + $page->assertSee('Sign In') + ->assertNoJavaScriptErrors() + ->click('Forgot Password?') + ->fill('email', 'nuno@laravel.com') + ->click('Send Reset Link') + ->assertSee('We have emailed your password reset link!'); + + Notification::assertSent(ResetPassword::class); +}); +``` + +### Smoke Testing + +Quickly validate multiple pages have no JavaScript errors: + + +```php +$pages = visit(['/', '/about', '/contact']); + +$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs(); +``` + +### Visual Regression Testing + +Capture and compare screenshots to detect visual changes. + +### Test Sharding + +Split tests across parallel processes for faster CI runs. + +### Architecture Testing + +Pest 4 includes architecture testing (from Pest 3): + + +```php +arch('controllers') + ->expect('App\Http\Controllers') + ->toExtendNothing() + ->toHaveSuffix('Controller'); +``` + +## Common Pitfalls + +- Not importing `use function Pest\Laravel\mock;` before using mock +- Using `assertStatus(200)` instead of `assertSuccessful()` +- Forgetting datasets for repetitive validation tests +- Deleting tests without approval +- Forgetting `assertNoJavaScriptErrors()` in browser tests \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index fc58b42..28203af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,12 +12,15 @@ This application is a Laravel application and its main Laravel ecosystems packag - php - 8.4 - laravel/framework (LARAVEL) - v13 - laravel/prompts (PROMPTS) - v0 +- laravel/sanctum (SANCTUM) - v4 +- larastan/larastan (LARASTAN) - v3 - laravel/boost (BOOST) - v2 - laravel/mcp (MCP) - v0 - laravel/pail (PAIL) - v1 - laravel/pint (PINT) - v1 - pestphp/pest (PEST) - v4 - phpunit/phpunit (PHPUNIT) - v12 +- rector/rector (RECTOR) - v2 ## Skills Activation @@ -103,12 +106,12 @@ This project has domain-specific skills available. You MUST activate the relevan - Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic. - Use array shape type definitions in PHPDoc blocks. -=== herd rules === +=== tests rules === -# Laravel Herd +# Test Enforcement -- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available. -- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start `, `herd php:list`). Run `herd list` to discover all available commands. +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. === laravel/core rules === diff --git a/CLAUDE.md b/CLAUDE.md index fc58b42..28203af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,12 +12,15 @@ This application is a Laravel application and its main Laravel ecosystems packag - php - 8.4 - laravel/framework (LARAVEL) - v13 - laravel/prompts (PROMPTS) - v0 +- laravel/sanctum (SANCTUM) - v4 +- larastan/larastan (LARASTAN) - v3 - laravel/boost (BOOST) - v2 - laravel/mcp (MCP) - v0 - laravel/pail (PAIL) - v1 - laravel/pint (PINT) - v1 - pestphp/pest (PEST) - v4 - phpunit/phpunit (PHPUNIT) - v12 +- rector/rector (RECTOR) - v2 ## Skills Activation @@ -103,12 +106,12 @@ This project has domain-specific skills available. You MUST activate the relevan - Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic. - Use array shape type definitions in PHPDoc blocks. -=== herd rules === +=== tests rules === -# Laravel Herd +# Test Enforcement -- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available. -- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start `, `herd php:list`). Run `herd list` to discover all available commands. +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. === laravel/core rules === diff --git a/GEMINI.md b/GEMINI.md index fc58b42..28203af 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -12,12 +12,15 @@ This application is a Laravel application and its main Laravel ecosystems packag - php - 8.4 - laravel/framework (LARAVEL) - v13 - laravel/prompts (PROMPTS) - v0 +- laravel/sanctum (SANCTUM) - v4 +- larastan/larastan (LARASTAN) - v3 - laravel/boost (BOOST) - v2 - laravel/mcp (MCP) - v0 - laravel/pail (PAIL) - v1 - laravel/pint (PINT) - v1 - pestphp/pest (PEST) - v4 - phpunit/phpunit (PHPUNIT) - v12 +- rector/rector (RECTOR) - v2 ## Skills Activation @@ -103,12 +106,12 @@ This project has domain-specific skills available. You MUST activate the relevan - Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic. - Use array shape type definitions in PHPDoc blocks. -=== herd rules === +=== tests rules === -# Laravel Herd +# Test Enforcement -- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available. -- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start `, `herd php:list`). Run `herd list` to discover all available commands. +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. === laravel/core rules === diff --git a/composer.json b/composer.json index 71a28c5..c84c49a 100644 --- a/composer.json +++ b/composer.json @@ -10,9 +10,9 @@ ], "license": "MIT", "require": { - "php": "^8.3", + "php": "^8.4", "guzzlehttp/guzzle": "^7.10.0", - "laravel/framework": "^13.0", + "laravel/framework": "^13.2", "laravel/sanctum": "^4.3.1", "laravel/tinker": "^3.0", "predis/predis": "^2.4.1", @@ -20,15 +20,15 @@ "treblle/treblle-laravel": "^6.0.1" }, "require-dev": { - "fakerphp/faker": "^1.23", + "fakerphp/faker": "^1.24.1", "larastan/larastan": "^3.9.3", - "laravel/boost": "^2.0", - "laravel/pail": "^1.2.5", - "laravel/pint": "^1.27", - "mockery/mockery": "^1.6", - "nunomaduro/collision": "^8.6", + "laravel/boost": "^2.4.1", + "laravel/pail": "^1.2.6", + "laravel/pint": "^1.29", + "mockery/mockery": "^1.6.12", + "nunomaduro/collision": "^8.9.1", "nunomaduro/phpinsights": "^2.14.0", - "pestphp/pest": "^4.4", + "pestphp/pest": "^4.4.3", "pestphp/pest-plugin-laravel": "^4.1", "rector/rector": "^2.3.9" }, diff --git a/composer.lock b/composer.lock index 695ce1b..8cf1548 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "03e4badeeb5d80f9be49f59c96c716a3", + "content-hash": "830cd89bf960021649c2080ca8adb1cf", "packages": [ { "name": "brick/math", @@ -12222,7 +12222,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.3" + "php": "^8.4" }, "platform-dev": {}, "plugin-api-version": "2.9.0"