Compare commits
211 commits
main
...
boost-test
| Author | SHA1 | Date | |
|---|---|---|---|
| a4cc75d697 | |||
| 9a0328d884 | |||
| f4c3f446da | |||
| be13421b7a | |||
| feb4a21e6a | |||
| ae3f5185fe | |||
| 1a93c74815 | |||
| fc268f0c7f | |||
| 9ed3b6c676 | |||
| daa8d1e0fa | |||
| 0766e9dc8f | |||
| 3582a3928c | |||
| bdb72b65d6 | |||
| 6f08526c7b | |||
| e31767cfd0 | |||
| 55576a6971 | |||
| cb97e6f591 | |||
| e10bd7741d | |||
| 376324d71a | |||
| bed0ded667 | |||
| cfa1289c9d | |||
| 79d3bd8261 | |||
| 514f098a96 | |||
| e7f49ccf84 | |||
| 6dca27a703 | |||
| 9c50fbd09b | |||
| 0e6aa52f2e | |||
| a395037058 | |||
| 4421d34a43 | |||
| 8e7d3e1dca | |||
| bb11d7aa78 | |||
| 891b21ba61 | |||
| 4a3d0c46a8 | |||
| 5c49b2cc4f | |||
| 6d63e68908 | |||
| 05af05e171 | |||
| 37bd6cdbdd | |||
| 1df3a27c98 | |||
| 0063727790 | |||
| a82a1558bd | |||
| 61073f4fcd | |||
| 709568f1ed | |||
| cc368c7102 | |||
| dc83df4105 | |||
| 0d2b0124ee | |||
| 20ae9a2307 | |||
| 95f10be9d4 | |||
| 5e4e2f5faf | |||
| 62289d2222 | |||
| fcbd2aa9c0 | |||
| fcf184dd1d | |||
| abe60df95f | |||
| 9c2c109d51 | |||
| aca627ff78 | |||
| b26baaaf86 | |||
| 8f933db9fa | |||
| 2617d41ef3 | |||
| 1f8f03e6d8 | |||
| 20ec692160 | |||
| a42a79861d | |||
| 8cfd5fbe0f | |||
| 6f35e031fa | |||
| 6be00bb21e | |||
| 66fc714ca0 | |||
| 4d68a97889 | |||
| 9c28cf2903 | |||
| f3b84c725d | |||
| f9364a8960 | |||
| 15e58526a8 | |||
| 15a0ff55a3 | |||
| c8a46bb9b3 | |||
| 2bbfc7fa08 | |||
| 98356465d5 | |||
| ed7ce3ec32 | |||
| 341b57c3bf | |||
| 7f57f16425 | |||
| 90c87899a2 | |||
| 2d46f774b3 | |||
| bad76036f1 | |||
| e3462241d3 | |||
| d3786aaed6 | |||
| 0a5603330a | |||
| 43758922ad | |||
| 2ac8578fe0 | |||
| af416b1b7c | |||
| a771d8a5d1 | |||
| e7f5f71005 | |||
| 90eff3cc99 | |||
| 726375c00d | |||
| e7904682b6 | |||
| fa86b9c4e6 | |||
| 909f7abaa4 | |||
| 5c10d51fba | |||
| 1dcf0a8cdf | |||
| 1fdf7ea9cc | |||
| fc368cdc46 | |||
| 8ce012d7f7 | |||
| 53a5990c3a | |||
| e02f5f5c2a | |||
| 2b7a8b4b99 | |||
| 3d4302b49c | |||
| 59ceab8185 | |||
| 0c25d663f8 | |||
| 13091c4ec6 | |||
| ac552a4530 | |||
| 7c0edb9507 | |||
| b0cd8d25a3 | |||
| 0a1d56fc69 | |||
| 8f82f86966 | |||
| 2900c85b33 | |||
| 64f2f1c3ca | |||
| 78680d06bc | |||
| bfe0b0010c | |||
| cce81c1a85 | |||
| afdde63d9f | |||
| 7383c3e01f | |||
| 91114cb67b | |||
| f667532ff7 | |||
| 4d24e5c3fa | |||
| ef8de85088 | |||
| dd7d1740ca | |||
| 07559b87e5 | |||
| 68de1fd224 | |||
| 195227a443 | |||
| cbf1a4ce9b | |||
| 37ecd9faa1 | |||
| 70375f8139 | |||
| 2cc333f1df | |||
| 753456368c | |||
| 7658483863 | |||
| 3c72e50adf | |||
| bbbeea6cde | |||
| 382f181ced | |||
| 152ee2b5a2 | |||
| 166257810d | |||
| 11291807a3 | |||
| 1d55dff5f4 | |||
| 3080c5dcfe | |||
| 53b0dd9f1e | |||
| 86e6de84ef | |||
| 6ba04fe009 | |||
| 61bde4baaa | |||
| 58749030bf | |||
| b64c1ef366 | |||
| b7f2de4f92 | |||
| 2529118bcc | |||
| 05ececd879 | |||
| 8855f74bf0 | |||
| b2cb3d8e8e | |||
| 92cf677d57 | |||
| 6d77bc6332 | |||
| e8d92e771a | |||
| e833d1c75a | |||
| dea8599b1e | |||
| 8afc31c806 | |||
| aaa39004bc | |||
| 289140c630 | |||
| d15f30bcbf | |||
| ab031e8d76 | |||
| 39d394b7fb | |||
| 3daeaccb4e | |||
| cc7a629193 | |||
| ccba793b4c | |||
| 52d0e6f0fd | |||
| 8e96abc88d | |||
| b3f02510b5 | |||
| b7c5c98128 | |||
| 985fcedb04 | |||
| 52d121085a | |||
| 3ba74409ff | |||
| 742476bdcb | |||
| 90d7bfdf37 | |||
| 31b83970b5 | |||
| 41dbb04963 | |||
| f8b5722a78 | |||
| 0111ee2ac4 | |||
| 198da8a11c | |||
| 8c0d139ad3 | |||
| 84e59314a0 | |||
| fed1916b24 | |||
| eacfd828f0 | |||
| 57756ce6ae | |||
| 40c360f766 | |||
| 554e4002df | |||
| 6c7c782a1d | |||
| b2c11a52ad | |||
| 3208556047 | |||
| d9329aca92 | |||
| 699cdf7f8e | |||
| 33f3aaf37f | |||
| fd284db1c2 | |||
| 04243860ce | |||
| e92b042906 | |||
| caa4bb0919 | |||
| a93fde3a3c | |||
| f62ea18e75 | |||
| a738fed493 | |||
| c5867c7926 | |||
| cfea429840 | |||
| 0cd539c180 | |||
| 6712e06042 | |||
| 2526560e44 | |||
| 31ae87f358 | |||
| 899bb42964 | |||
| 3100643eac | |||
| 38ca7ed069 | |||
| 43a568ef5a | |||
| 63d4ae17f3 | |||
| e74f81b656 | |||
| 93d76b3c69 | |||
| 34169a9feb |
783 changed files with 62268 additions and 84 deletions
433
.agents/AGENTS.md
Normal file
433
.agents/AGENTS.md
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
# Arcadia API - Directives Architecturales (Source de Vérité)
|
||||
|
||||
> Ce document est la **source de vérité** pour tout agent AI intervenant sur ce projet.
|
||||
> Il est dérivé du code source réel sur disque, pas de la documentation théorique.
|
||||
|
||||
## 1. Vue d'ensemble
|
||||
|
||||
**Arcadia API** est une API Laravel 13 dont l'architecture par défaut a été profondément modifiée pour respecter :
|
||||
- **Clean Architecture** (cercles de dépendances)
|
||||
- **Domain-Driven Design** (Entités riches, Value Objects, Aggregats)
|
||||
- **Architecture Hexagonale** (Ports & Adapters)
|
||||
|
||||
Le namespace `App\` de Laravel n'est **pas utilisé**. Tout le code applicatif vit sous `src/` avec des namespaces PSR-4 dédiés.
|
||||
|
||||
**Stack technique** : PHP 8.2+ (strict typing), Laravel 13, Sanctum 4, Pest 4, PHPStan level 10 (Larastan), Pint (PSR-12).
|
||||
|
||||
---
|
||||
|
||||
## 2. Topologie des Dossiers
|
||||
|
||||
```
|
||||
src/
|
||||
├── Application/ # Cas d'usage et Bounded Contexts
|
||||
│ ├── Arcadia/
|
||||
│ │ └── Usecase/
|
||||
│ │ └── {Context}/ # ex: Employees
|
||||
│ │ └── {Action}/ # ex: FindAll - contient Input, Output, ActionName, Presenter, View
|
||||
│ └── Security/ # Bounded Context Sécurité & Auth API
|
||||
├── Domain/ # Coeur métier - AUCUNE dépendance framework
|
||||
│ ├── Models/
|
||||
│ │ ├── Entities/ # Entités riches (readonly, immutables)
|
||||
│ │ └── ValueObjects/ # Value Objects groupés par contexte
|
||||
│ └── Ports/
|
||||
│ ├── Repositories/ # Interfaces des repositories
|
||||
│ └── Services/ # Interfaces des services externes
|
||||
├── Infra/ # Implémentations techniques (Laravel, Eloquent)
|
||||
│ ├── Adapters/
|
||||
│ │ └── Repositories/ # Implémentations Eloquent des Ports
|
||||
│ ├── Database/
|
||||
│ │ └── Models/ # Modèles Eloquent (anémiques, mapping BDD uniquement)
|
||||
│ ├── Http/
|
||||
│ │ └── Middleware/ # Middlewares d'infrastructure (ex: VerifyHmacSignature)
|
||||
│ ├── Providers/ # Service Providers (binding Port -> Adapter)
|
||||
│ └── Usecase/ # LaravelExecutor (exécution transactionnelle des Use Cases)
|
||||
├── Presentation/ # Points d'entrée
|
||||
│ ├── Api/
|
||||
│ │ └── V1/
|
||||
│ │ ├── Contracts/ # Interfaces API (Controller, ApiResponse, ApiResponseDto)
|
||||
│ │ ├── Dto/ # DTOs de sérialisation HTTP (conversion types -> scalaires)
|
||||
│ │ └── Http/
|
||||
│ │ ├── Controllers/ # Controllers invokables (Single Action)
|
||||
│ │ ├── Requests/ # FormRequests (validation + construction Input)
|
||||
│ │ └── Responses/ # Objets Response (transformation View -> JsonResponse)
|
||||
│ └── Web/
|
||||
│ ├── Http/Controllers/ # Controllers web (Blade)
|
||||
│ └── resources/views/ # Templates Blade
|
||||
└── Shared/ # Code transverse et socle DDD
|
||||
├── DDD/
|
||||
│ ├── Usecase/ # Interfaces : Input, Output, Usecase, Presenter, Executor
|
||||
│ │ └── Builder/ # Fluent builder typesafe pour l'exécution des Use Cases
|
||||
│ ├── Entity.php # Interface Entity<T extends EntityId>
|
||||
│ ├── EntityId.php # Classe abstraite readonly (Stringable + Equatable)
|
||||
│ ├── Repository.php # Interface Repository<TId, TEntity>
|
||||
│ ├── Directory.php # Interface Directory<TId, TEntity> (read-only lookup)
|
||||
│ ├── Event.php # Interface marqueur pour événements domaine
|
||||
│ ├── DomainEventDispatcher.php
|
||||
│ └── Equatable.php # Interface equals(mixed $other): bool
|
||||
├── Paginated.php # VO générique Paginated<T> (immutable, clone des items)
|
||||
└── Pagination.php # VO de pagination (ofPage, ofOffsetAndLimit, ofIndexes)
|
||||
```
|
||||
|
||||
### Autoloading PSR-4 (`composer.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Application\\": "src/Application/",
|
||||
"Domain\\": "src/Domain/",
|
||||
"Infra\\": "src/Infra/",
|
||||
"Presentation\\": "src/Presentation/",
|
||||
"Shared\\": "src/Shared/"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Règles de Dépendance (STRICTES)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Domain + Shared │ <- Ne dépend de RIEN (pas de Laravel)
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Application │ <- Dépend de Domain + Shared
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Presentation │ <- Dépend de Application + Domain + Shared
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Infra │ <- Dépend de Domain + Application + Shared
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Règle absolue** : AUCUNE couche ne doit importer `Infra\`. Les dépendances pointent toujours vers l'intérieur.
|
||||
|
||||
> [!CAUTION]
|
||||
> `Domain\` ne doit jamais importer Illuminate, Eloquent, HTTP, ou quoi que ce soit de Laravel.
|
||||
> `Application\` non plus - sauf `Shared\`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Patron des Use Cases
|
||||
|
||||
Chaque Use Case réside dans `Application/Arcadia/Usecase/{Context}/{Action}/` et contient **obligatoirement** :
|
||||
|
||||
| Fichier | Rôle | Interface implémentée |
|
||||
| ------------------ | ----------------------- | ------------------------------------------- |
|
||||
| `Input.php` | DTO entrant | `Shared\DDD\Usecase\Input` |
|
||||
| `Output.php` | DTO sortant | `Shared\DDD\Usecase\Output` |
|
||||
| `{ActionName}.php` | Logique d'orchestration | `Shared\DDD\Usecase\Usecase<Input, Output>` |
|
||||
|
||||
Fichiers additionnels optionnels dans le même namespace :
|
||||
- `{Name}Presenter.php` - implémente `Shared\DDD\Usecase\Presenter<Output, View>`
|
||||
- `{Name}View.php` - DTO de présentation produit par le Presenter
|
||||
- DTO internes (ex: `PaginatedEmployee.php`)
|
||||
|
||||
### Signature obligatoire du Use Case
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Arcadia\Usecase\{Context}\{Action};
|
||||
|
||||
use Domain\Ports\Repositories\{Repository};
|
||||
use Shared\DDD\Usecase\Input as BaseInput;
|
||||
use Shared\DDD\Usecase\Usecase;
|
||||
|
||||
/** @implements Usecase<\Shared\DDD\Usecase\Input, \Shared\DDD\Usecase\Output> */
|
||||
final readonly class {ActionName} implements Usecase
|
||||
{
|
||||
public function __construct(
|
||||
private {Repository} $repository
|
||||
) {}
|
||||
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
assert($input instanceof Input);
|
||||
// ... logique d'orchestration
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Signature obligatoire du Presenter
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Arcadia\Usecase\{Context}\{Action};
|
||||
|
||||
use Shared\DDD\Usecase\Output as UsecaseOutput;
|
||||
use Shared\DDD\Usecase\Presenter;
|
||||
|
||||
/** @implements Presenter<\Shared\DDD\Usecase\Output, {View}> */
|
||||
final readonly class {Name}Presenter implements Presenter
|
||||
{
|
||||
public function present(UsecaseOutput $output): object
|
||||
{
|
||||
assert($output instanceof Output);
|
||||
return {View}::fromOutput($output);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Exécution des Use Cases (Fluent Builder)
|
||||
|
||||
Les Use Cases sont exécutés via un builder typesafe :
|
||||
|
||||
```php
|
||||
use Shared\DDD\Usecase\Builder\UsecaseBuilder;
|
||||
|
||||
$input = new Input(...); // Instancié par le contrôleur
|
||||
|
||||
$result = UsecaseBuilder::using($this->executor)
|
||||
->do($this->usecase) // Usecase
|
||||
->from($input) // Input
|
||||
->with(new Presenter()) // Presenter
|
||||
->run(); // Exécute et retourne le View
|
||||
```
|
||||
|
||||
`LaravelExecutor` (`Infra\Usecase\LaravelExecutor`) enveloppe l'exécution dans :
|
||||
- Une **transaction DB** (`$database->transaction(...)`)
|
||||
- Un **logging PSR** (debug start, info success, error failure)
|
||||
|
||||
---
|
||||
|
||||
## 6. Couche de Présentation (API)
|
||||
|
||||
### Controller (Single Action, Invokable)
|
||||
|
||||
```php
|
||||
final readonly class {Name}Controller
|
||||
{
|
||||
public function __construct(
|
||||
private {UseCase} $usecase,
|
||||
private LaravelExecutor $executor,
|
||||
) {}
|
||||
|
||||
public function __invoke({Name}Request $request): JsonResponse
|
||||
{
|
||||
$input = new Input(
|
||||
// ... construction depuis $request
|
||||
);
|
||||
|
||||
/** @var {View} $response */
|
||||
$response = UsecaseBuilder::using($this->executor)
|
||||
->do($this->usecase)
|
||||
->from($input)
|
||||
->with(new {Name}Presenter())
|
||||
->run();
|
||||
|
||||
return (new {Name}Response($response))->present();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Request (FormRequest)
|
||||
|
||||
Les `FormRequest` valident exclusivement les données HTTP (format, types, contraintes). Elles ne doivent pas instancier le `Input` du Use Case et ne doivent pas définir de méthode `authorize()` (l'authentification et les autorisations sont assurées en amont par la chaîne de middlewares / filtres).
|
||||
|
||||
### Exception Handling & API Versioning
|
||||
|
||||
Les exceptions sont traitées de manière contextualisée et versionnée :
|
||||
- `Presentation\Api\Exceptions\ApiExceptionDispatcher` aiguille la requête selon son préfixe (`api/v1/*` -> `Presentation\Api\V1\Exceptions\ApiExceptionRenderer`).
|
||||
- Les renderers de version (`Presentation\Api\V1\Exceptions\ApiExceptionRenderer`) transforment les exceptions en réponses JSON conformes aux schémas OpenAPI v1 :
|
||||
- `\DomainException` (invariants métiers du Domaine) -> HTTP 422 (`Unprocessable Entity`, `domain_invariant`).
|
||||
- `ValidationException` (FormRequests) -> HTTP 422 (`Unprocessable Entity`, `invalidParams`).
|
||||
- `NotFoundException` (Use Cases) -> HTTP 404 (`Resource Not Found`).
|
||||
- `\InvalidArgumentException` (SPL) -> HTTP 500 (`Internal Server Error` - non intercepté car signale un bug de contrat interne/développeur).
|
||||
- `Presentation\Web\Exceptions\WebExceptionRenderer` intercepte les requêtes web/Blade.
|
||||
- `bootstrap/app.php` enregistre la chaîne de délégation.
|
||||
|
||||
## 7. Architecture Hexagonale (Ports & Adapters)
|
||||
|
||||
### Port (Domain)
|
||||
|
||||
```php
|
||||
// Domain\Ports\Directories\EmployeeDirectory
|
||||
/** @extends Directory<ArcadiaEmployeeId, Employee> */
|
||||
interface EmployeeDirectory extends Directory
|
||||
{
|
||||
/** @return Paginated<Employee> */
|
||||
public function findAll(Pagination $pagination, EmployeeSorter $sorter): Paginated;
|
||||
public function find(ArcadiaEmployeeId $id): ?Employee;
|
||||
}
|
||||
```
|
||||
|
||||
### Adapter (Infra)
|
||||
|
||||
```php
|
||||
// Infra\Adapters\Directories\EloquentEmployeeDirectory
|
||||
final class EloquentEmployeeDirectory implements EmployeeDirectory
|
||||
{
|
||||
// Utilise Infra\Database\Models\Employee (Eloquent)
|
||||
// Convertit vers Domain\Models\Entities\Employee via toDomain()
|
||||
}
|
||||
```
|
||||
|
||||
### Binding (Service Provider)
|
||||
|
||||
```php
|
||||
// Infra\Providers\DomainServiceProvider
|
||||
$this->app->bind(EmployeeDirectory::class, EloquentEmployeeDirectory::class);
|
||||
$this->app->singleton(UsecaseExecutor::class, LaravelExecutor::class);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Entités de Domaine
|
||||
|
||||
Les entités sont `final readonly class` implémentant `Shared\DDD\Model\Entity<T>` :
|
||||
- Pas d'Eloquent, pas de framework
|
||||
- Typage strict, Value Objects pour les identifiants et données métier
|
||||
- Immutables, construites par le constructeur
|
||||
|
||||
```php
|
||||
/** @implements Entity<ArcadiaEmployeeId> */
|
||||
final readonly class Employee implements Entity
|
||||
{
|
||||
public function __construct(
|
||||
public ArcadiaEmployeeId $publicEmployeeNumber,
|
||||
public string $lastName,
|
||||
// ...
|
||||
) {}
|
||||
|
||||
public function id(): ArcadiaEmployeeId
|
||||
{
|
||||
return $this->publicEmployeeNumber;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Value Objects
|
||||
|
||||
- Étendent `Shared\DDD\Model\EntityId` pour les identifiants (readonly, Stringable, Equatable)
|
||||
- Peuvent valider via regex dans le constructeur (ex: `ArcadiaEmployeeId` valide `/^\d{7,8}$/`)
|
||||
- Groupés par contexte sous `Domain\Models\ValueObjects\`
|
||||
|
||||
---
|
||||
|
||||
## 9. Modèles Eloquent (Infrastructure)
|
||||
|
||||
Les modèles Eloquent sont **anémiques** - dédiés exclusivement au mapping BDD :
|
||||
|
||||
```php
|
||||
// Infra\Database\Models\Employee
|
||||
final class Employee extends Model
|
||||
{
|
||||
// table, primaryKey, fillable, casts
|
||||
// Pas de logique métier
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Authentification API (HMAC)
|
||||
|
||||
L'API utilise un système de **signature HMAC-SHA256** (style AWS) avec protection **anti-replay** :
|
||||
|
||||
- Headers requis : `X-Client-Id`, `X-Timestamp`, `X-Signature`
|
||||
- Tolérance timestamp : +/-5 minutes (300 secondes)
|
||||
- Payload signé : `METHOD|URI|TIMESTAMP|BODY`
|
||||
- Middleware : `Infra\Http\Middleware\VerifyHmacSignature` (alias `auth.hmac`)
|
||||
- Clients API : stockés via `Infra\Database\Models\ApiClient` (extends Authenticatable + HasApiTokens)
|
||||
|
||||
---
|
||||
|
||||
## 11. Configuration Laravel Spécifique
|
||||
|
||||
| Élément | Chemin |
|
||||
| ----------------------- | -------------------------------------------------------------------------- |
|
||||
| Vues Blade | `src/Presentation/Web/resources/views/` |
|
||||
| `config/view.php` paths | `base_path('src/Presentation/Web/resources/views')` |
|
||||
| Bootstrap | `bootstrap/app.php` (routing, middleware, exceptions) |
|
||||
| Providers | `bootstrap/providers.php` -> `[DomainServiceProvider, AppServiceProvider]` |
|
||||
| Middleware aliases | `bootstrap/middlewares.php` -> `auth.hmac` |
|
||||
| Routes API | `routes/api.php` -> `routes/apiv1.php` (prefix `v1`) |
|
||||
| Routes Web | `routes/web.php` |
|
||||
| PHPStan | Level 10, paths `src/` + `tests/`, extensions Larastan + Carbon |
|
||||
|
||||
---
|
||||
|
||||
## 12. Conventions de Code
|
||||
|
||||
- **`declare(strict_types=1)`** obligatoire dans chaque fichier
|
||||
- **PSR-12** via Laravel Pint
|
||||
- **PHPStan level 10** - generics `@template`, `@implements`, `@extends` requis
|
||||
- Classes finales par défaut (`final class`, `final readonly class`)
|
||||
- Constructeurs promus (constructor property promotion)
|
||||
- Attribut `#[\Override]` sur les méthodes implémentant une interface
|
||||
- **Classes/Interfaces/Exceptions natives PHP** : pas d'import `use` dans le namespace global (ex: ne pas faire `use Override;`, `use Stringable;`, `use RuntimeException;`, `use Closure;`). Utiliser directement les FQCN globaux `#[\Override]`, `\Stringable`, `\RuntimeException`, `\InvalidArgumentException`, `\Closure`, `\DateTimeImmutable`, etc.
|
||||
- Tests avec **Pest 4** (PHPUnit 12)
|
||||
|
||||
---
|
||||
|
||||
## 13. Shared / Socle DDD - Référence Rapide
|
||||
|
||||
| Classe/Interface | Namespace | Rôle |
|
||||
| ---------------------------------- | ---------------------------- | ---------------------------------------------- |
|
||||
| `Input` | `Shared\DDD\Usecase` | Interface marqueur pour DTOs entrants |
|
||||
| `Output` | `Shared\DDD\Usecase` | Interface marqueur pour DTOs sortants |
|
||||
| `Usecase<TInput, TOutput>` | `Shared\DDD\Usecase` | Interface générique pour les Use Cases |
|
||||
| `Presenter<TOutput, T>` | `Shared\DDD\Usecase` | Interface générique pour les Presenters |
|
||||
| `Executor` | `Shared\DDD\Usecase` | Interface `execute(): object` |
|
||||
| `UsecaseExecutor<TInput, TOutput>` | `Shared\DDD\Usecase\Builder` | Interface pour l'exécuteur (transaction + log) |
|
||||
| `UsecaseBuilder` | `Shared\DDD\Usecase\Builder` | Point d'entrée du fluent builder |
|
||||
| `Entity<T>` | `Shared\DDD\Model` | Interface pour entités de domaine |
|
||||
| `EntityId` | `Shared\DDD\Model` | Classe abstraite readonly pour identifiants |
|
||||
| `AggregateRoot<T>` | `Shared\DDD\Model` | Interface pour racines d'agrégats (écriture) |
|
||||
| `ReadModel` | `Shared\DDD\Model` | Interface marqueur pour modèles de lecture |
|
||||
| `Repository<TId, TAggregate>` | `Shared\DDD\Port` | Interface CRUD & requêtes d'agrégat |
|
||||
| `Directory<TId, TReadModel>` | `Shared\DDD\Port` | Interface lecture seule pour ReadModels |
|
||||
| `Equatable` | `Shared\DDD\Model` | Interface `equals(mixed): bool` |
|
||||
| `Event` | `Shared\DDD\Event` | Interface marqueur pour événements domaine |
|
||||
| `DomainEventDispatcher` | `Shared\DDD\Event` | Interface `dispatch(Event): void` |
|
||||
| `Pagination` | `Shared` | VO de pagination (MAX_PAGE_SIZE=10000) |
|
||||
| `Paginated<T>` | `Shared` | Résultat paginé immutable |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 14. Checklist - Avant de Générer du Code
|
||||
|
||||
- [ ] `declare(strict_types=1)` en tête de fichier
|
||||
- [ ] Le fichier est dans la bonne couche (`Domain`, `Application`, `Presentation`, `Infra`, `Shared`)
|
||||
- [ ] Pas d'import croisé interdit (Domain n'importe jamais Infra/Presentation/Application sauf Shared)
|
||||
- [ ] Use Case = 3 fichiers minimum (Input, Output, Action) dans le même namespace
|
||||
- [ ] Aliasing `use Shared\DDD\Usecase\Input as BaseInput` + `assert($input instanceof Input)`
|
||||
- [ ] PHPDoc generics pour PHPStan (`@implements`, `@template`, `@return`)
|
||||
- [ ] Classes `final` (ou `final readonly`) par défaut
|
||||
- [ ] `#[\Override]` sur les méthodes d'interface
|
||||
- [ ] Entités de domaine : pas d'Eloquent, riches, immutables
|
||||
- [ ] Modèles Eloquent : anémiques, dans `Infra\Database\Models\`
|
||||
- [ ] Controller : invokable, single action, aucune logique métier
|
||||
- [ ] FormRequest : validation HTTP uniquement, pas de `toInput()`, pas de `authorize()`
|
||||
- [ ] Response : utilise les DTOs de `Presentation\Api\V1\Dto\` pour la sérialisation
|
||||
|
||||
---
|
||||
|
||||
## 15. Directives de Tests (F.I.R.S.T. & Pattern GWT)
|
||||
|
||||
Chaque test écrit dans ce projet doit obligatoirement respecter les principes **F.I.R.S.T.** (Fast, Isolated, Repeatable, Self-validating, Timely) et la structure **GWT** (Given - When - Then) avec les commentaires explicites `// GIVEN`, `// WHEN`, `// THEN` :
|
||||
|
||||
```php
|
||||
it('initializes from basket with the correct state', function (): void {
|
||||
// GIVEN
|
||||
$basketId = new BasketId(Uuid::uuid7());
|
||||
$basket = new Basket($basketId);
|
||||
|
||||
// WHEN
|
||||
$builder = OrderBuilder::fromBasket($basket);
|
||||
|
||||
// THEN
|
||||
expect($builder)
|
||||
->toBeInstanceOf(OrderBuilderWithBasket::class)
|
||||
->and($builder->isCompleted())->toBeFalse();
|
||||
});
|
||||
```
|
||||
11
.agents/mcp_config.json
Normal file
11
.agents/mcp_config.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"laravel-boost": {
|
||||
"command": "php",
|
||||
"args": [
|
||||
"artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
104
.agents/skills/infer-conventions/SKILL.md
Normal file
104
.agents/skills/infer-conventions/SKILL.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
name: infer-conventions
|
||||
description: "Use this skill to analyze how a Laravel application is actually written and record its conventions as shared rules. Trigger when the user wants to detect, infer, document, or standardize project conventions or coding style, set up or grow `.ai/rules`, resolve mixed or conflicting patterns (e.g. \"are we using Form Requests or inline validation?\"), or onboard agents and teammates to \"how we do things here\". Covers: a systematic sweep of ~49 Laravel convention dimensions (validation, models, architecture, testing, frontend, database, console), open-ended house-pattern discovery, conflict reporting, and recording rules scoped to the right paths via the Boost `record-rule` MCP tool. Do not use for one-off code review, enforcing formatting a linter already handles, or editing `.ai/rules` files by hand."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Infer Conventions
|
||||
|
||||
Learn how this application writes Laravel, then record what you learn as durable, path-scoped rules other agents will read. You are documenting reality, not improving it.
|
||||
|
||||
## Ground Rules (read before you start)
|
||||
|
||||
- Consistency first. The codebase's majority style is the convention. Never judge it, never propose a "better" pattern, never record what the code should do. If the app validates inline everywhere, that is the rule, even if Form Requests would be nicer.
|
||||
- Skip what an active tool produces, keep what a tool would fight. Inspect the project's Pint and Rector configuration first; a Rector transformation is tooling-owned only when its package and relevant rule or set are installed and enabled. Active tools may rewrite code toward one canonical form: `$casts` to `casts()`, `$fillable` to attributes, magic accessors to the `Attribute` class, pipe-string rules to arrays, `$signature` to `#[Signature]`, named migrations to anonymous, and many more. When the app already sits at an active tool's target form, the tool owns it, so record nothing. But when the app deliberately holds a form an active tool would refactor away, such as legacy `getXxxAttribute()` accessors the `Attribute` class would replace, no tool can reproduce that choice and an agent defaults the other way. That against-the-grain hold is exactly what to record.
|
||||
- Record decisions, not defaults. A consistent pattern earns a rule only when it reflects a choice: the app took one valid option where the framework or common practice offered others, or the pattern would surprise a competent agent. Framework defaults steer nothing, so skip them: anonymous migrations, `$signature` commands, `ShouldQueue` jobs, `casts()` on Laravel 11+, named routes, Rule objects in `app/Rules`, and `Mail::fake()` or `Bus::fake()` to isolate framework services. A real fork is not enough on its own. Weigh the side the app took, and record only the side an agent would not reach for by itself: inline closures everywhere, legacy accessors, a bespoke query layer. Watch for the false fork too. "No Mockery" next to facade fakes is not a choice against Mockery, because they double different things. The test for every candidate: without this rule, would the next agent plausibly write it differently? Only "yes" earns a rule.
|
||||
- Architecture choices are the gold. Record presence and deliberate absence. The structural pattern the app commits to is the highest-signal convention and the one no tool can decide: Action classes and how they are invoked (`handle` / `execute` / `__invoke`), service objects, dedicated query objects exposing `builder()`, DTOs (spatie/laravel-data vs readonly classes), Form Request validation vs inline, an events and listeners spine vs direct calls, and domain or module folders. Also record a consistent non-pattern, such as "query Eloquent directly in controllers, no repository layer", so the next agent matches the app's altitude instead of over-engineering.
|
||||
- Never duplicate `.ai/rules`. Read `.ai/rules/index.md` and the area files before the sweep. A dimension already covered there is marked done and skipped.
|
||||
- Evidence or silence. A convention needs at least 3 consistent examples and no meaningful rival to become a candidate. Every Step 1 verdict applies this bar.
|
||||
- The recorded rule states the convention, nothing else. One or two imperative lines: this project does X, so do X here. Keep detection evidence out. No counts, ratios, current usage, file lists, or example paths, because that is proof for the confirm step, not part of the rule. One short syntax fragment at most, and point to `search-docs` for API details.
|
||||
|
||||
## Process
|
||||
|
||||
Each step ends on a checkable completion criterion. Do not advance until it holds.
|
||||
|
||||
Fan out when you can. The sweep is embarrassingly parallel. If your environment can spawn subagents (a Task, dispatch, or equivalent tool), do Step 0 yourself, then hand each checklist group (A to J) and the architecture map to its own subagent. Each subagent runs the greps, reads a few representative files, and returns structured verdicts (dimension, verdict, evidence, proposed glob / title / note). You aggregate, dedupe, then run Steps 3 to 5. It is far faster on a real app. No subagents available? Run the steps in sequence, with the same bar and the same output.
|
||||
|
||||
### Step 0: Orient
|
||||
|
||||
Read `composer.json` (installed packages tell you which checklist groups apply), the `pint.json` / PHPStan / Rector config, `.ai/rules/index.md` if present, and most important, map the `app/` tree. List every directory under `app/` (and any `Modules/`, `src/`, `packages/`, or domain root). Every folder beyond Laravel's default skeleton (`Http`, `Models`, `Providers`, `Console`, `Exceptions`) is a structural pattern the app committed to and a high-value rule waiting to be written: `Actions`, `Services`, `Data` or DTOs, `Queries`, `Repositories`, `ViewModels`, `Pipelines`, `Support`, `Enums`, `Contracts`, `Observers`, or `Domain` and module roots. Note each one. You will confirm how it is used in Step 2.
|
||||
|
||||
This app has no Livewire/Inertia/Flux packages installed. Treat the frontend group as likely API-only: confirm from `resources/views` before spending time there, and skip the Livewire/Inertia/Flux dimensions.
|
||||
|
||||
Done when: you have the applicable checklist groups, the dimensions already recorded in `.ai/rules`, and a list of every non-default `app/` directory mapped to the pattern it represents.
|
||||
|
||||
### Step 1: Predefined sweep
|
||||
|
||||
Open `references/checklist.md` and work every applicable dimension using its search hints. Give each exactly one verdict:
|
||||
|
||||
- Pattern. Clears the bar, rival under ~20% of sites, and reflects a real choice (passes the decisions-not-defaults test). A recording candidate. Cite 2 to 3 example files.
|
||||
- Conflict. Both styles present in meaningful numbers. Report the split with counts and example files. Never record a preferred winner while the code remains mixed, even in yolo, because that would describe an aspiration rather than reality. Record only if the user identifies a stable path or context boundary that explains both styles; otherwise defer until the code is reconciled.
|
||||
- Default. Consistent, but a framework or common-practice default the agent already writes unprompted. Skip it as a no-op, not a convention.
|
||||
- No signal. Under the bar: feature unused, or too few examples. Skip silently (one summary line at most).
|
||||
- Tooling-owned or Already-recorded. Skip per the ground rules.
|
||||
|
||||
Done when: every applicable dimension carries exactly one of those verdicts.
|
||||
|
||||
### Step 2: Open-ended pass
|
||||
|
||||
First, close out the architecture map from Step 0. For every non-default `app/` directory you listed, confirm how the pattern is used and apply the same evidence and decisions-not-defaults tests as Step 1. Generator-standard or sparsely used directories such as `Rules`, `Observers`, `Mail`, and `Notifications` are signals to inspect, not automatic conventions. Make genuine structural patterns candidates: Action classes invoked via `handle` / `execute` / `__invoke`, Services constructor-injected, `Queries` objects exposing `builder(): Builder`, DTOs as readonly classes or spatie/laravel-data, module or domain folders as the unit of organization. Scope each qualifying pattern to its own directory glob. Also record a consistent deliberate absence, such as "no repository layer, controllers query Eloquent directly", so the next agent matches the app's altitude.
|
||||
|
||||
Then find what else makes this codebase itself: base or abstract classes most code extends, traits used everywhere, tenancy or authorization scoping woven through queries, naming schemes, and custom helpers. Same evidence bar, cite files. Record every genuine structural pattern, and cap the other house findings at ~5 so the pass stays high-signal.
|
||||
|
||||
Done when: every non-default `app/` directory from Step 0 has a verdict, and the pass has produced its cited house findings (or concluded there are none).
|
||||
|
||||
### Step 3: Confirm
|
||||
|
||||
Present every candidate in one batch. Per item: dimension, verdict, evidence (counts and files), and the exact proposed `glob` or `globs` / `title` / `note`. Conflicts are presented as questions about an existing context boundary or deferred cleanup, not as a choice of future style.
|
||||
|
||||
Default mode is confirm: record only what the user approves. Switch to yolo only when the invocation said so ("yolo", "don't ask", "just record them"), then record all pattern candidates without asking. Conflicts still go to the user in yolo.
|
||||
|
||||
Done when: every candidate is approved, rejected, or (conflicts) decided.
|
||||
|
||||
### Step 4: Record
|
||||
|
||||
Make one `record-rule` call for each glob an approved convention applies to. Choose the most specific globs that cover the cited evidence from the mapping table below; if a convention spans models and migrations, record it under both domains so agents discover it from either path. The `note` is the bare convention: strip every trace of detection (see the ground rule). If `record-rule` is unavailable (rules disabled), report the full rule text so the user can enable `BOOST_RULES_ENABLED` or add it by hand.
|
||||
|
||||
Record this:
|
||||
|
||||
> Accessors and mutators: use the legacy magic-method style (`getXxxAttribute()` / `setXxxAttribute()`), not the `Attribute` class. Match it in models.
|
||||
|
||||
Not this:
|
||||
|
||||
> Accessors/mutators use the legacy magic-method style; the `Attribute`-class style is not used anywhere (13 legacy, 0 Attribute-class), e.g. `app/Models/Post.php`. Match the legacy style in existing models.
|
||||
|
||||
Done when: every approved item has a successful tool response, and any failure is reported with its rule text.
|
||||
|
||||
### Step 5: Summarize
|
||||
|
||||
List recorded rules (file and title), conflicts the user deferred, notable no-signals, and remind the user to commit `.ai/rules` so their team and agents share the conventions.
|
||||
|
||||
## Glob mapping
|
||||
|
||||
Attach each rule to the most specific path that covers its evidence. Never a lazy `app/**` when a subtree fits. Match the glob to where the code actually lives, which is not the same in a default skeleton and in a modular or DDD layout. Use the Step 0 `app/` map to pick the real path.
|
||||
|
||||
Examples:
|
||||
|
||||
- Models: `app/Models/**` in a default app, or `app/Modules/Blog/Models/**` / `src/Domain/Blog/**` in a modular one.
|
||||
- Controllers, routing, validation, responses: `app/Http/**`, or `app/Modules/*/Http/**` when each module owns its HTTP layer.
|
||||
- Actions, Services, DTOs: `app/Actions/**`, `app/Services/**`, `app/Data/**`, or the module path the app actually uses.
|
||||
- Tests: `tests/**`.
|
||||
- Migrations and database: `database/migrations/**`.
|
||||
- Truly app-wide (rare, e.g. auth retrieval): `app/**`.
|
||||
|
||||
`record-rule` takes one glob. When a convention genuinely spans two domains (e.g. UUID keys touch models and migrations), call it once per domain with the same title and note; mentioning another path in the note does not make the rule discoverable there.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- Rules disabled or `record-rule` missing: detection is read-only, so Steps 0 to 3 still run, and recording falls back to the manual path in Step 4.
|
||||
- Tiny or fresh app: most dimensions land on no-signal. Say so honestly ("not enough code to infer conventions yet") and record nothing.
|
||||
- Huge app: each dimension is a bounded grep plus a handful of file reads. Sample representative files, do not read everything.
|
||||
- Re-runs: reading `.ai/rules` in Step 0 makes re-runs incremental, so only new or undecided dimensions surface.
|
||||
- Non-standard layout (modules, DDD): the open-ended pass catches the layout itself as convention #1. Adapt the globs in the mapping table to the observed paths.
|
||||
137
.agents/skills/infer-conventions/references/checklist.md
Normal file
137
.agents/skills/infer-conventions/references/checklist.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Detection Checklist
|
||||
|
||||
Every dimension here is a genuine fork: Laravel offers two or more valid approaches, the app's choice changes what the next agent writes, and no active project tool can pick for you. Left out on purpose: pure formatting (Pint owns it), any form an installed and enabled Rector rule rewrites to one canonical shape (`$casts` to `casts()`, `$fillable` to attributes, pipe-string rules to arrays, named to anonymous migrations, `$signature` to `#[Signature]`), and framework defaults any agent writes unprompted (`ShouldQueue` jobs, relation return types, `HasFactory`).
|
||||
|
||||
Each item gives the fork, then a hint (a grep or dir to spot which side the app takes). Hints are only a start. Read the matched files, never record on a raw count. Apply the ground rules to every verdict: a consistent choice that is a default or a tool's target form is not a pattern. Rows tagged (architecture) are the highest-signal, so record presence and deliberate absence.
|
||||
|
||||
---
|
||||
|
||||
## A. Validation & HTTP input
|
||||
|
||||
1. Validation entry point: inline `$request->validate()` vs Form Request classes vs `Validator::make()`.
|
||||
- Hint: `ls app/Http/Requests`; grep `->validate(` / `Validator::make(` in `app/Http/Controllers`.
|
||||
2. Custom rule location: invokable rule objects in `app/Rules` vs inline closures vs `Validator::extend()` in a provider. Rule objects are the default `make:rule` path, so record only if the app leans on closures or `Validator::extend` instead. "No rule objects" alone is just no-signal.
|
||||
- Hint: `ls app/Rules`; grep `Validator::extend` in `app/Providers`.
|
||||
3. Typed input retrieval: typed getters (`$request->string()`, `->integer()`, `->enum()`, `->date()`) vs raw `$request->input()` / dynamic properties.
|
||||
- Hint: grep `->string(` / `->integer(` / `->enum(` vs `->input(` in `app/Http`.
|
||||
4. Custom messages/attributes: `lang/*/validation.php` vs Form Request `messages()` / `attributes()` methods.
|
||||
- Hint: `ls lang`; grep `function messages`, `function attributes` in `app/Http/Requests`.
|
||||
|
||||
## B. Controllers & routing
|
||||
|
||||
5. Controller shape: invokable single-action (`__invoke`) vs resource controllers vs plain multi-method.
|
||||
- Hint: grep `__invoke` in controllers; `Route::resource` / `apiResource` vs verb routes.
|
||||
6. Business-logic location (architecture): fat controllers vs delegated to Actions / Services / Jobs.
|
||||
- Hint: read a few controller methods; `ls app/Actions app/Services`.
|
||||
7. Route handler style: closures in `routes/*.php` vs controller classes.
|
||||
- Hint: count `function ()` vs `::class` in `routes/web.php`, `routes/api.php`.
|
||||
8. Middleware assignment: route/group `->middleware()` vs controller `HasMiddleware::middleware()` vs `#[Middleware]` attribute.
|
||||
- Hint: grep `implements HasMiddleware`, `#[Middleware(` in controllers vs `->middleware(` in routes.
|
||||
9. Route model binding: implicit (type-hinted models) vs explicit `Route::bind` vs manual `findOrFail`.
|
||||
- Hint: typed model params in signatures vs `findOrFail(` in controllers; grep `Route::bind`.
|
||||
10. Rate limiting: named `RateLimiter::for()` + `throttle:name` vs inline `throttle:60,1`.
|
||||
- Hint: grep `RateLimiter::for` in providers vs `throttle:` in route files.
|
||||
|
||||
## C. Authorization
|
||||
|
||||
11. Authorization home: Gates (`Gate::define`) vs Policy classes in `app/Policies`.
|
||||
- Hint: `ls app/Policies`; grep `Gate::define` in `app/Providers`.
|
||||
12. Authorization call site: `$this->authorize()` / `Gate::authorize()` vs `$user->can()` vs `can` middleware vs `#[Authorize]` vs `@can` in Blade.
|
||||
- Hint: grep `authorize(`, `->can(`, `middleware('can:`, `#[Authorize(`, `@can(`.
|
||||
|
||||
## D. Eloquent & models
|
||||
|
||||
13. Mass assignment: `$fillable` allow-list vs `$guarded` block-list.
|
||||
- Hint: grep `protected $fillable` / `protected $guarded` in `app/Models`.
|
||||
14. Accessors/mutators: modern `Attribute` class vs legacy `getXxxAttribute()` / `setXxxAttribute()`. Record a legacy hold, it goes against the tool's grain.
|
||||
- Hint: grep `: Attribute` / `Attribute::make` vs `function get[A-Z].*Attribute` in `app/Models`.
|
||||
15. Primary keys: auto-increment vs `HasUuids` vs `HasUlids`.
|
||||
- Hint: grep `HasUuids` / `HasUlids` in `app/Models`; migration `id()` vs `uuid('id')`.
|
||||
16. Custom casts: dedicated `CastsAttributes` classes (`app/Casts`) vs inline `Attribute` vs built-in cast strings.
|
||||
- Hint: `ls app/Casts`; grep `Cast::class`, `AsStringable::class` in models.
|
||||
17. Data/query layer (architecture): Eloquent directly in controllers vs repositories vs dedicated query objects (e.g. classes exposing `builder(): Builder`).
|
||||
- Hint: `ls app/Repositories app/Queries`; see where non-trivial queries are built.
|
||||
18. Query scopes: local `scope`/`#[Scope]` methods vs dedicated builder classes.
|
||||
- Hint: grep `function scope` / `#[Scope]` in models; `ls app/*/Builders`.
|
||||
19. Model events: observers (`app/Observers`, `#[ObservedBy]`) vs `booted()` closures vs event classes.
|
||||
- Hint: `ls app/Observers`; grep `booted`, `::observe`, `#[ObservedBy]`.
|
||||
20. Eager-load posture: explicit per-query `->with()` vs model-level `$with` defaults. Treat `preventLazyLoading()` separately as a development guard because it can complement either posture.
|
||||
- Hint: grep `protected $with`, `->with(`, and separately `preventLazyLoading` in `app/`.
|
||||
|
||||
## E. Architecture & organization
|
||||
|
||||
21. Action/Service structure (architecture): Action classes (invoked via `handle` / `execute` / `__invoke`) vs service objects vs neither. Cross-check the Step 0 `app/` map: any `Actions`/`Services`/`Pipelines`/`Jobs`-as-actions folder is this pattern, so record how it is invoked.
|
||||
- Hint: `ls app/` (the whole tree, not just `Actions`/`Services`); grep the invocation method in the folder you find.
|
||||
22. DTOs (architecture): spatie/laravel-data vs plain readonly classes vs arrays everywhere.
|
||||
- Hint: `ls app/Data`; grep `extends Data`, `readonly class` in `app/`.
|
||||
23. Dependency acquisition: constructor/method injection vs `app()` / `resolve()` / `App::make()` service location.
|
||||
- Hint: grep `app(` / `resolve(` / `::make(` in `app/` vs promoted constructor deps.
|
||||
24. Decoupling: events + listeners vs direct service calls.
|
||||
- Hint: `ls app/Events app/Listeners`; grep `event(`, `::dispatch(`.
|
||||
25. Helper vs facade idiom: global helpers (`config()`, `auth()`, `response()`) vs facades (`Config::`, `Auth::`, `Response::`).
|
||||
- Hint: ratio of `config(` vs `Config::` (etc.) across `app/`.
|
||||
26. Namespace layout (architecture): default `app/` skeleton vs domain/module folders (`app/Domain/**`, modules).
|
||||
- Hint: `ls app/`, look for `Domain/`, `Modules/`, bounded-context folders.
|
||||
27. Enums: backed vs pure; case naming; where they live.
|
||||
- Hint: `ls app/Enums`; grep `enum .*: string`, `enum .*: int`.
|
||||
|
||||
## F. Frontend & views
|
||||
|
||||
No Livewire/Inertia/Flux package is installed. This app may be API-only. Confirm from `resources/views` before sweeping, and treat the Livewire/Flux dimensions as not applicable.
|
||||
|
||||
28. Frontend stack: Blade+Livewire vs Inertia (Vue/React/Svelte) vs Blade-only / API + separate SPA.
|
||||
- Hint: `composer.json` + `package.json`; `ls resources/js/pages`, `resources/views`.
|
||||
29. Blade composition: class `<x-*>` components vs anonymous components (`@props`) vs `@include` partials.
|
||||
- Hint: `ls app/View/Components`; grep `<x-`, `@include` in `resources/views`.
|
||||
32. Localization: short keys (`lang/*/*.php` + `__('messages.welcome')`) vs JSON string keys (`lang/*.json` + `__('Full sentence')`).
|
||||
- Hint: `ls lang`; grep dotted `__('` vs sentence keys.
|
||||
|
||||
## G. Database & migrations
|
||||
|
||||
33. Foreign keys: `foreignId()->constrained()` vs `foreignIdFor(Model::class)` vs manual `foreign()->references()->on()`.
|
||||
- Hint: grep `foreignId(`, `foreignIdFor(`, `->foreign(` in `database/migrations`.
|
||||
34. `down()` methods: real reverse logic vs omitted / one-way migrations.
|
||||
- Hint: grep `function down` vs the migration count.
|
||||
35. Enum storage: DB `enum()` column vs `string()` + PHP-enum cast on the model.
|
||||
- Hint: grep `->enum(` in migrations vs string columns cast to enums.
|
||||
36. Transactions: `DB::transaction(fn ...)` closure vs manual `beginTransaction` / `commit` / `rollBack`.
|
||||
- Hint: grep `DB::transaction`, `beginTransaction` in `app/`.
|
||||
37. Idempotent writes: `upsert` / `updateOrCreate` / `firstOrCreate` vs find-then-save.
|
||||
- Hint: grep `upsert(`, `updateOrCreate(`, `firstOrCreate(` in `app/`.
|
||||
|
||||
## H. Testing
|
||||
|
||||
38. Framework: Pest (`it()` / `test()` / `expect()`) vs PHPUnit classes.
|
||||
- Hint: `ls tests/Pest.php`; grep `it(` / `test(` vs `extends TestCase`.
|
||||
39. DB reset: `RefreshDatabase` vs `DatabaseTruncation` vs `DatabaseMigrations`.
|
||||
- Hint: grep those trait names in `tests/`.
|
||||
40. Fixtures: compare how equivalent test-owned records are created, such as factories vs manual inserts. Track seeders separately for shared reference data because `$this->seed()` commonly and legitimately coexists with factories.
|
||||
- Hint: grep `::factory(` and direct inserts in `tests/`; separately inspect `$this->seed(` calls and what those seeders provide.
|
||||
41. Collaborator isolation: how the app doubles its own classes, Mockery `mock()` / `spy()` vs real integration. Ignore facade fakes like `Mail::fake()` here, they isolate framework services by default and are not a fork against Mockery.
|
||||
- Hint: grep `->mock(`, `->spy(`, `Mockery::` in `tests/`.
|
||||
42. Endpoint assertions: array `assertJson([...])` / `assertJsonFragment` vs fluent `AssertableJson`.
|
||||
- Hint: grep `AssertableJson`, `assertJsonFragment` in `tests/`.
|
||||
|
||||
## I. Responses & API resources
|
||||
|
||||
43. Response shape: API Resource classes vs `response()->json()` vs returning models/arrays directly.
|
||||
- Hint: `ls app/Http/Resources`; grep `JsonResource`, `->json(` in controllers.
|
||||
44. Resource relationship inclusion: `whenLoaded()` guards vs unconditional relationship access. Do not count ordinary scalar attributes as rivals to conditional relationships, and evaluate general `when()` fields separately.
|
||||
- Hint: compare relationship fields using `whenLoaded(` with unconditional relationship property access in `app/Http/Resources`.
|
||||
45. Pagination contracts: within comparable endpoint categories, length-aware `paginate()` vs `simplePaginate()` vs `cursorPaginate()`. These have different totals, navigation, ordering, and performance contracts, so record only a stable path-scoped API policy, never a project-wide majority.
|
||||
- Hint: grep those in `app/`, then group matches by endpoint type and client contract before comparing them.
|
||||
46. Web redirects/URLs: `route('name')` vs `url('/path')` vs `action([...])`.
|
||||
- Hint: grep `route('`, `url('/`, `action([` in `app/Http` and views.
|
||||
|
||||
## J. Strings, collections & dates
|
||||
|
||||
47. Iteration idiom: `collect()->map()->filter()` pipelines vs `array_map` / `foreach`.
|
||||
- Hint: grep `collect(`, `->map(` vs `array_map`, `foreach` density in `app/`.
|
||||
48. String API: fluent `Str::of()->...` (Stringable) vs static `Str::` vs native (`trim`, `strtoupper`).
|
||||
- Hint: grep `Str::of(` vs `Str::` vs native string funcs.
|
||||
49. Dates: compare equivalent construction call styles (`now()` / `today()` helpers vs `Carbon::`) separately from the application's mutable/immutable date policy. `Date::use(CarbonImmutable::class)` can make helpers return immutable dates, so those signals are complementary rather than conflicting.
|
||||
- Hint: grep `now(` and `Carbon::` for call style; separately inspect `CarbonImmutable` and `Date::use` for mutability policy.
|
||||
|
||||
---
|
||||
|
||||
Genuine forks only. Every row survived the "no tool can decide this, and it isn't the default" filter. Give each applicable dimension exactly one verdict: pattern, conflict, default, no-signal, tooling-owned, or already-recorded. The rows tagged (architecture) are where the highest-value rules come from.
|
||||
51
.agents/skills/laravel-audit-architecture/SKILL.md
Normal file
51
.agents/skills/laravel-audit-architecture/SKILL.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
name: laravel-audit-architecture
|
||||
description: >
|
||||
Audit a Laravel application's architecture: boundaries, responsibility,
|
||||
coupling, duplication, and maintainability. Use when auditing architecture.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Architecture Audit
|
||||
|
||||
Audit the architecture of the Laravel application. **Avoid cargo-cult recommendations** such as "create a repository for every model" or "move everything into services."
|
||||
|
||||
List applicable rules first:
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules --domain=architecture --applicable
|
||||
```
|
||||
|
||||
## What to look for
|
||||
|
||||
- Clear violations of application boundaries: domain logic leaking into controllers, routes, or views; persistence concerns in presentation layers.
|
||||
- Duplicated business logic: the same rule implemented in several places with divergence risk.
|
||||
- Excessive controller responsibility: controllers doing far more than orchestrating HTTP.
|
||||
- Business logic in inappropriate layers: rules embedded in views, middleware, or migrations.
|
||||
- Tightly coupled components: classes that are hard to test or change because of hidden dependencies.
|
||||
- Repeated patterns that have become maintenance problems: copy-pasted blocks that should be shared.
|
||||
- Inconsistent architectural conventions: half the app using one pattern and half another without reason.
|
||||
- Unnecessary abstractions: interfaces, repositories, or services that add indirection without value.
|
||||
- Dangerous service/repository patterns: abstraction layers that create complexity rather than value.
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
Every architecture finding needs at least:
|
||||
|
||||
- The specific class(es) and file/line ranges.
|
||||
- Why the pattern is a problem in this codebase (not just "best practice").
|
||||
- A concrete improvement that fits the existing style.
|
||||
|
||||
## False positives
|
||||
|
||||
- Small controllers that orchestrate a couple of calls are fine.
|
||||
- A service layer is not automatically bad; only flag it when it adds complexity without value.
|
||||
- Do not impose a single architecture style the codebase does not otherwise use.
|
||||
|
||||
## Severity guidance
|
||||
|
||||
- `high`: structure actively blocks maintenance or causes bugs (e.g. duplicated critical logic diverging).
|
||||
- `medium`: clear maintainability problem with real cost.
|
||||
- `low`: stylistic or mild structure concern.
|
||||
- `info`: observations for future work.
|
||||
50
.agents/skills/laravel-audit-conventions/SKILL.md
Normal file
50
.agents/skills/laravel-audit-conventions/SKILL.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
name: laravel-audit-conventions
|
||||
description: >
|
||||
Audit a Laravel application for framework conventions: anti-patterns,
|
||||
version-appropriate APIs, and misuse of framework features. Use when
|
||||
auditing Laravel conventions.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Conventions Audit
|
||||
|
||||
Audit the application for Laravel framework conventions. **The rules must be version-aware whenever the installed version is known.**
|
||||
|
||||
List applicable rules first:
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules --domain=conventions --applicable
|
||||
```
|
||||
|
||||
## What to look for
|
||||
|
||||
- Framework anti-patterns: patterns that fight the framework's intended flow.
|
||||
- Unnecessary custom code where standard Laravel mechanisms are clearly better: custom auth loops instead of guards, manual pagination instead of `paginate`, custom file handling instead of storage, etc.
|
||||
- Version-inappropriate APIs when the version is known: deprecated or removed methods, old-style helpers, `facades` used in ways removed in the installed version.
|
||||
- Incorrect framework assumptions: `config()` keys that don't exist, middleware that isn't applied, routes that don't match registered patterns.
|
||||
- Misuse of framework lifecycle/features: service providers doing work in `register()`, observers/mutators causing side effects, cache keys without invalidation, queueable misuse, scheduling misconfig.
|
||||
- Configuration, validation, routing, jobs, events, policies, resources, commands, notifications, mail, queues, scheduling, and caching used in problematic ways.
|
||||
- Abandoned runtime dependencies (`AUD-DEP-002`) or license conflicts for a commercial product (`AUD-DEP-003`) — only with `composer show` / license evidence, not because a package is old.
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
Every conventions finding needs at least:
|
||||
|
||||
- The installed Laravel version (from `project_info`).
|
||||
- The code in question (file + line/range).
|
||||
- The idiomatic Laravel alternative and why it is better.
|
||||
|
||||
## False positives
|
||||
|
||||
- Custom code may exist for good reasons the framework mechanism cannot serve — verify before recommending replacement.
|
||||
- Only recommend replacement when the standard mechanism is clearly better.
|
||||
- Do not flag style preferences as findings.
|
||||
|
||||
## Severity guidance
|
||||
|
||||
- `high`: usage that is broken or will break on upgrade.
|
||||
- `medium`: clear anti-pattern with maintenance cost.
|
||||
- `low`: minor convention drift.
|
||||
- `info`: observations.
|
||||
51
.agents/skills/laravel-audit-database/SKILL.md
Normal file
51
.agents/skills/laravel-audit-database/SKILL.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
name: laravel-audit-database
|
||||
description: >
|
||||
Audit a Laravel application's database layer: schema, indexes, relationships,
|
||||
migrations, and query efficiency. Use when auditing the database layer.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Database Audit
|
||||
|
||||
Audit the database layer of the Laravel application. **Where possible, use structured schema information** (via the `database_schema` tool) rather than relying only on migrations.
|
||||
|
||||
List applicable rules first:
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules --domain=database --applicable
|
||||
```
|
||||
|
||||
## What to look for
|
||||
|
||||
- Missing or suspicious indexes when evidence supports them: frequently filtered/joined columns without indexes.
|
||||
- Schema inconsistencies: columns that contradict domain invariants or usage.
|
||||
- Inefficient relationships: relationship definitions that force expensive queries.
|
||||
- Suspicious migrations: destructive operations, deadlocks, or migrations that drop data without a path back.
|
||||
- Duplicate data modeling: the same fact stored in multiple tables/columns.
|
||||
- Bad foreign key choices: missing FKs, wrong ON DELETE behavior, or FKs to the wrong tables.
|
||||
- Nullable/non-nullable mismatches where they create risk.
|
||||
- Query inefficiencies: full-table scans on large tables, missing index coverage for common filters.
|
||||
- Relationship definitions inconsistent with schema or usage: `foreignId` that does not match the relationship, missing pivot table indexes, etc.
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
Every database finding needs at least:
|
||||
|
||||
- The specific table/column (or migration file + line).
|
||||
- The query pattern or relationship that is affected.
|
||||
- Schema facts from the `database_schema` tool or migrations.
|
||||
|
||||
## False positives
|
||||
|
||||
- Small tables do not need indexes on every column.
|
||||
- An index that already exists (or is covered by a composite index) is not missing.
|
||||
- Do not flag nullable columns when the domain allows null.
|
||||
|
||||
## Severity guidance
|
||||
|
||||
- `high`: schema/relationship mismatch causing real bugs or data integrity risk.
|
||||
- `medium`: clear inefficiency or inconsistency with real impact.
|
||||
- `low`: minor schema hygiene.
|
||||
- `info`: observations.
|
||||
143
.agents/skills/laravel-audit-dsa/SKILL.md
Normal file
143
.agents/skills/laravel-audit-dsa/SKILL.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
---
|
||||
name: laravel-audit-dsa
|
||||
description: >
|
||||
Run a read-only, orchestrated audit of a Laravel codebase for data
|
||||
structures, state representation, algorithms, and ownership. Use when asked
|
||||
for a DSA audit, subsystem inventory, or Aaron-style bounded agent audit.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel DSA / organizing-model audit
|
||||
|
||||
Audit the Laravel application for **materially useful simplifications** in data structures, state representation, control flow, algorithms, and ownership.
|
||||
|
||||
This is an **audit-only** exercise. Do not edit files, run mutating commands, implement recommendations, commit, or push. Read-only inspection and Laravel Auditor context tools are allowed.
|
||||
|
||||
You are the **coordinator**. Continue until every identifiable subsystem has been reviewed and the final audit is validated.
|
||||
|
||||
Credit: this workflow follows Aaron Francis's bounded, read-only subsystem audit method.
|
||||
|
||||
## Tools to use first
|
||||
|
||||
```bash
|
||||
php artisan auditor:context subsystems
|
||||
php artisan auditor:context project_info
|
||||
php artisan auditor:rules --applicable
|
||||
```
|
||||
|
||||
Also use `routes`, `models`, `database_schema`, `policies_authorization`, `jobs_events_schedules`, and `tests` as needed. Prefer these over raw guesses.
|
||||
|
||||
Map each finding to a stable rule when one fits (`AUD-DSA-001`–`AUD-DSA-004`, or a core domain rule). Put `subsystem` and `priority` (`p0`–`p3`) in finding `metadata`.
|
||||
|
||||
## 1. Establish the coverage contract
|
||||
|
||||
Inspect the repository and inventory every identifiable subsystem. Start from `auditor:context subsystems`, then add any leftover `app/` areas it did not list.
|
||||
|
||||
Give each subsystem:
|
||||
|
||||
- a stable ID and descriptive name
|
||||
- an exact ownership boundary
|
||||
- its key implementation files
|
||||
- relevant public interfaces, major call sites, and tests
|
||||
- a status: `queued`, `in review`, `recommend`, or `skip`
|
||||
|
||||
Include HTTP, models, auth, database, jobs/queues, events, console, mail/notifications, tests, config, frontend/Livewire/Inertia/Filament when present, and generated-contract ownership where relevant.
|
||||
|
||||
Create one canonical report containing:
|
||||
|
||||
- the subsystem inventory
|
||||
- confirmed opportunities
|
||||
- explicit skip decisions
|
||||
- cross-cutting patterns
|
||||
- duplicates and superseded findings
|
||||
- final priorities and dependencies
|
||||
- an audit log
|
||||
|
||||
Treat this inventory as the coverage contract. Do not assume a catch-all row proves coverage.
|
||||
|
||||
## 2. Run bounded subsystem reviews
|
||||
|
||||
Use fresh, read-only agents where available. Give every worker **one** distinct subsystem with an exact, non-overlapping ownership boundary.
|
||||
|
||||
Keep concurrency bounded to the number of lanes you can actually coordinate. Use one consolidated wait. Do not interrupt productive workers just because they are slow. Close completed workers after harvesting results.
|
||||
|
||||
Each worker receives this brief:
|
||||
|
||||
> Review the assigned subsystem for **at most two** materially useful simplifications in its data structures, state representation, or organizing model.
|
||||
>
|
||||
> Inspect implementation, public interfaces, major call sites, and existing tests. Stay inside the assigned ownership boundary. You may name a cross-subsystem concern; do not expand scope to solve it.
|
||||
>
|
||||
> Look for:
|
||||
>
|
||||
> - scattered booleans or nullable fields that permit invalid combinations and should become a state machine or discriminated union
|
||||
> - repeated assumptions about object shape that need a shared typed model
|
||||
> - duplicated branching that a small map, registry, reducer, or command model would remove
|
||||
> - unclear state or behavior ownership that a small module boundary would clarify
|
||||
> - repeated scans, transformations, or lookups where a more appropriate collection or index would materially simplify behavior
|
||||
> - lifecycle, concurrency, or async states whose representation permits stale or contradictory state
|
||||
>
|
||||
> Do not force an abstraction. Prefer boring local code when it is already clear.
|
||||
>
|
||||
> Do not recommend changes solely for stylistic consistency, hypothetical extensibility, minor line-count reduction, or moving existing branching behind a new type.
|
||||
>
|
||||
> Return at most two opportunities. If nothing clearly meets the threshold, return `skip`.
|
||||
>
|
||||
> For every recommendation, provide:
|
||||
>
|
||||
> 1. Verdict: recommend or skip
|
||||
> 2. Evidence with exact file and line references
|
||||
> 3. Current complexity or invalid states
|
||||
> 4. Proposed representation and why it is simpler
|
||||
> 5. Smallest credible implementation scope, including affected files and interfaces
|
||||
> 6. Regression risks and migration concerns
|
||||
> 7. Existing and additional validation required
|
||||
> 8. Confidence: high, medium, or low
|
||||
|
||||
## 3. Validate and synthesize
|
||||
|
||||
Independently verify every finding against the current repository before accepting it.
|
||||
|
||||
Reject, narrow, or demote recommendations that are vague, duplicate another finding, misunderstand intentional Laravel/framework semantics, or merely relocate complexity.
|
||||
|
||||
Record skips as completed coverage. Deduplicate overlapping findings and assign each accepted recommendation to one authoritative subsystem.
|
||||
|
||||
Continue opening bounded review batches until every inventory row is complete.
|
||||
|
||||
## 4. Audit the audit
|
||||
|
||||
Before finishing, run fresh independent passes for:
|
||||
|
||||
- repository coverage and missing subsystem boundaries
|
||||
- duplication and ownership overlap
|
||||
- materiality and over-abstraction
|
||||
- schema completeness
|
||||
- dependency-aware priority ranking
|
||||
|
||||
If the coverage pass finds a real omission, add an explicit subsystem row and audit it. Do not hide it by broadening a previously completed boundary.
|
||||
|
||||
Rank the final recommendations by concrete impact, confidence, implementation effort, blast radius, and prerequisites. Identify the best first implementation slices.
|
||||
|
||||
Then assign each accepted finding a tier (put `metadata.priority` on the finding). Every promoted ID appears exactly once.
|
||||
|
||||
- **P0** — reachable wrong-record, lost-update, authorization, durable-state, or permanently incomplete-operation risks
|
||||
- **P1** — concrete boundary failures and high-leverage ownership fixes (less immediate damage, or greater migration cost)
|
||||
- **P2** — useful invariant improvements with narrower impact or sensitive migration
|
||||
- **P3** — telemetry / diagnostics / maintainability; keep this tier small after materiality review
|
||||
|
||||
Then render:
|
||||
|
||||
```bash
|
||||
php artisan auditor:report --findings=storage/auditor-findings.json
|
||||
```
|
||||
|
||||
The report includes a priority synthesis. Set `metadata.priority` to `p0`–`p3` when you rank explicitly.
|
||||
|
||||
## Done only when
|
||||
|
||||
- every identifiable subsystem has been reviewed
|
||||
- every subsystem has a recommendation or explicit skip
|
||||
- every finding has complete evidence, scope, risk, and validation fields
|
||||
- duplicates and weak abstractions have been removed
|
||||
- priorities and dependencies are internally consistent
|
||||
- the repository remains unchanged
|
||||
183
.agents/skills/laravel-audit-performance/SKILL.md
Normal file
183
.agents/skills/laravel-audit-performance/SKILL.md
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
---
|
||||
name: laravel-audit-performance
|
||||
description: >
|
||||
Audit a Laravel application's performance deeply and safely: unnecessary
|
||||
queries, N+1, collection vs database work, memory amplification, repeated
|
||||
computation, I/O, queues, and rendering cost — verifying that every proposed
|
||||
optimization preserves behavior before reporting it. Use when auditing
|
||||
performance or investigating a slow path.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Performance Audit
|
||||
|
||||
Audit how the application spends time, memory, queries, and I/O — and report only optimizations that are **semantically verified** and **worth acting on**.
|
||||
|
||||
List applicable rules first:
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules --domain=performance --applicable
|
||||
```
|
||||
|
||||
Ecosystem rules (`AUD-LW-003`, `AUD-FIL-003`, `AUD-IN-003`) appear only when those packages are installed.
|
||||
|
||||
## The methodology
|
||||
|
||||
Work every candidate through this pipeline. A pattern that stops at any stage is not a finding.
|
||||
|
||||
```text
|
||||
Signal A suspicious shape (query in a loop, get()->count(), unbounded all())
|
||||
↓
|
||||
Context Who calls this? What route/job/command? What data volume? What else uses the result?
|
||||
↓
|
||||
Behavior What does the code actually do — accessors, casts, custom collections, callbacks?
|
||||
↓
|
||||
Verification Is the proposed replacement semantically equivalent FOR THIS USAGE?
|
||||
↓
|
||||
Impact Which resource does it save (queries, rows transferred, memory, CPU, I/O)? Is it meaningful?
|
||||
↓
|
||||
Finding Evidence-backed report with mechanism-based impact and justified severity
|
||||
```
|
||||
|
||||
**A suspicious pattern is not automatically a performance bug.**
|
||||
|
||||
### Worked example — when it IS a finding
|
||||
|
||||
```php
|
||||
$total = User::where('active', true)->get()->count();
|
||||
```
|
||||
|
||||
The collection is materialized only to be collapsed into one number. Every matching row is transferred and hydrated to produce what `User::where('active', true)->count()` returns as a single value. Verified: `$total` is a scalar; no other code uses the collection. Report via `AUD-PER-008`.
|
||||
|
||||
### Counter-example — same shape, NOT a finding
|
||||
|
||||
```php
|
||||
$users = User::where('active', true)->get();
|
||||
|
||||
$count = $users->count();
|
||||
|
||||
return view('users.index', compact('users', 'count'));
|
||||
```
|
||||
|
||||
Do **not** recommend `->count()` here. The collection is rendered by the view; replacing the aggregate would issue a second query or force a refactor. Deriving the count from already-loaded data is the cheapest correct option. This rule applies to every "obvious" optimization in this skill: check what else consumes the value first.
|
||||
|
||||
## What to investigate
|
||||
|
||||
Work through the areas relevant to this application. Use context tools (`routes`, `models`, `database_schema`, `jobs_events_schedules`) to find hot paths and data-volume sources before reading code.
|
||||
|
||||
### Database and Eloquent
|
||||
|
||||
- Materialized aggregates: `get()->count()/sum()/avg()/min()/max()`, `get()->isEmpty()/isNotEmpty()`, `get()->first()` where the collection has no other consumer (`AUD-PER-008`).
|
||||
- PHP doing database work: filtering, sorting, slicing, or deduplication of freshly queried rows that SQL could do (`AUD-PER-009`).
|
||||
- Relationship counts/existence answered by loading rows: `$post->comments->count()`, `->isNotEmpty()` vs `withCount()`/`withExists()`/`->relation()->count()` (`AUD-PER-010`).
|
||||
- Queries inside loops: per-item lookups, counts, writes without bulk forms (`AUD-PER-011`).
|
||||
- N+1 relationship access in loops, views, resources, jobs — verify the relationship is actually loaded lazily before reporting (`AUD-PER-001`).
|
||||
- Over-selection: full hydration of wide tables where few columns are consumed (`AUD-PER-013`).
|
||||
- Repeated identical queries within one lifecycle that can share one result (`AUD-PER-004`).
|
||||
|
||||
### Collections
|
||||
|
||||
- Chained transformations creating avoidable intermediates on large sets: `map()->filter()`, `filter()->first()` where `first(callable)` is equivalent (`AUD-PER-018`).
|
||||
- Multiple separate passes over one large collection where a single pass or a keyed lookup would do.
|
||||
- Semantic traps: `all()` returns the underlying items; `toArray()` recursively converts Arrayable items. They are interchangeable **only** when consumers tolerate the difference. Never swap them blindly.
|
||||
- Lazy vs eager: long chains over large datasets may suit `lazy()` collections — but only propose this when the chain is genuinely hot.
|
||||
|
||||
### Application flow
|
||||
|
||||
- Expensive synchronous work in request paths where a queue is clearly idiomatic (`AUD-PER-002`, `AUD-PER-005`).
|
||||
- Repeated expensive stable computations without a justified cache (`AUD-PER-007`).
|
||||
- Hot paths first: high-traffic routes, scheduled commands, queue workers. A slow admin page once a month is rarely worth the same attention.
|
||||
|
||||
### Data volume
|
||||
|
||||
- Unbounded retrieval on growth-prone paths: APIs, exports, reports, commands, jobs (`AUD-PER-012`). Match the mechanism to the workload: paginate/cursorPaginate for HTTP, chunkById/lazyById for read-modify-write, lazy/cursor for single-pass streaming.
|
||||
- Jobs processing unbounded datasets or carrying oversized serialized payloads (`AUD-PER-016`).
|
||||
|
||||
### I/O
|
||||
|
||||
- External HTTP calls inside loops, repeated identical requests, sequential independent calls that could batch (`AUD-PER-014`). Respect call dependencies and rate limits.
|
||||
- Storage/filesystem work repeated per item, or whole-file reads where streaming fits (`AUD-PER-015`). Remote disks make every call network I/O.
|
||||
|
||||
### Rendering
|
||||
|
||||
- Queries inside Blade loops/components, expensive helpers recomputed per iteration, stable values recomputed per render without caching (`AUD-PER-017`).
|
||||
- Livewire: per-render/update queries, computed properties that re-query on each subsequent update (they are memoized within a single request), oversized public properties serialized every cycle (`AUD-LW-003`).
|
||||
- Filament: select options loading whole tables, per-row closures querying, counts without `withCount` (`AUD-FIL-003`).
|
||||
- Inertia: shared props computed eagerly on every response including pages that never use them (`AUD-IN-003`).
|
||||
|
||||
Only investigate ecosystem areas when the package is installed.
|
||||
|
||||
## Verify semantic equivalence before recommending
|
||||
|
||||
Every recommendation must answer: **does the replacement produce identical observable results in this exact usage?** Check specifically:
|
||||
|
||||
- **Collection reuse** — is the materialized collection used anywhere else (view, return value, later loop)? If yes, most query-side rewrites do not apply.
|
||||
- **Accessors, casts, appended attributes** — PHP-side operations may see transformed values SQL never produces. Aggregate comparisons can differ.
|
||||
- **Comparison strictness** — Collection `where()`/`contains()` compare loosely by default; SQL compares differently. Null handling differs too.
|
||||
- **Ordering semantics** — collation, locale-aware sort callbacks, stability of sorts.
|
||||
- **Custom collection classes** — overridden methods (`count()`, `contains()`, `first()`) may have non-standard behavior.
|
||||
- **Model requirements** — narrowed selects still need primary keys, foreign keys, and anything accessors/casts touch.
|
||||
- **Side effects** — bulk writes skip model events unless explicitly handled; lazy loading timing changes when queries execute.
|
||||
|
||||
If equivalence cannot be established, either lower confidence and say so, or do not report.
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
Every performance finding needs at least:
|
||||
|
||||
- The specific site (file + line/range) and the enclosing method/route/job.
|
||||
- The cost driver with a source: iteration count origin, table growth expectation, call frequency — from routes/schema/jobs context, not vibes.
|
||||
- Why the current form performs the unnecessary work (what is transferred/computed and then discarded or repeated).
|
||||
- The verification note: what you checked to confirm the replacement is safe *here*.
|
||||
|
||||
Do not invent measurements. Describe mechanisms ("avoids transferring every matching row into PHP") rather than fabricating multipliers. Cite runtime evidence only if it actually exists.
|
||||
|
||||
## Severity guidance
|
||||
|
||||
Justify severity from reach × frequency × amplification:
|
||||
|
||||
- `high`: query explosion or heavy amplification on hot/high-volume paths (per-row queries over user-driven loops, unbounded retrieval on public endpoints, external calls multiplied by dataset size) — `AUD-PER-011` findings typically land here when loops are data-driven.
|
||||
- `medium`: clear unnecessary database work, avoidable materialization, repeated queries, oversized payloads, rendering-path queries with real traffic.
|
||||
- `low`: real but small inefficiencies — narrow-column wins, single-model count forms, pass-collapsing on modest collections.
|
||||
- `info`: observations worth noting without action.
|
||||
|
||||
A correct micro-optimization on a tiny dataset is not a finding at all.
|
||||
|
||||
## False positives — keep the report trustworthy
|
||||
|
||||
- The collection/result is reused after the suspicious call (the counter-example above).
|
||||
- Semantics depend on PHP-only behavior the replacement cannot reproduce.
|
||||
- Loops bounded by small fixed sets (config constants, enum values).
|
||||
- Inherently bounded tables (countries, settings) need no pagination.
|
||||
- Already-loaded relationships queried again would be *slower*, not faster.
|
||||
- Caching without a key/invalidation story; concurrency for dependent requests; eager loading everything "just in case" (over-eager loading is itself waste).
|
||||
|
||||
## Performance checklist
|
||||
|
||||
Run this checklist over each scoped area. Every checked box needs evidence before it becomes a finding.
|
||||
|
||||
```text
|
||||
□ Are queries executed unnecessarily?
|
||||
□ Are queries executed inside loops?
|
||||
□ Are relationships causing N+1 queries?
|
||||
□ Is data loaded that is never needed?
|
||||
□ Is a collection materialized unnecessarily?
|
||||
□ Is SQL work being performed in PHP?
|
||||
□ Is PHP work being performed where SQL is more appropriate?
|
||||
□ Are aggregates done in PHP unnecessarily?
|
||||
□ Are large datasets loaded into memory?
|
||||
□ Is pagination/chunking/lazy iteration appropriate?
|
||||
□ Are unnecessary columns selected?
|
||||
□ Are identical queries repeated?
|
||||
□ Are external HTTP calls repeated?
|
||||
□ Are Storage/filesystem operations repeated?
|
||||
□ Are jobs processing too much data?
|
||||
□ Is rendering causing repeated queries?
|
||||
□ Is Livewire state unnecessarily large?
|
||||
□ Is Filament performing repeated database work?
|
||||
□ Is the suggested optimization semantically equivalent?
|
||||
□ Is the optimization meaningful enough to report?
|
||||
```
|
||||
|
||||
The last two boxes gate everything above them.
|
||||
63
.agents/skills/laravel-audit-security/SKILL.md
Normal file
63
.agents/skills/laravel-audit-security/SKILL.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
name: laravel-audit-security
|
||||
description: >
|
||||
Audit a Laravel application's security boundaries: authorization, mass
|
||||
assignment, sensitive data, file/URL handling, CSRF, XSS, injection, and
|
||||
secrets. Use when auditing security or when asked to review security of a
|
||||
Laravel app.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Security Audit
|
||||
|
||||
Audit the security boundaries of the Laravel application. **Do not invent vulnerabilities merely because a pattern is uncommon.** Require context and evidence.
|
||||
|
||||
List applicable rules first:
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules --domain=security --applicable
|
||||
```
|
||||
|
||||
## What to look for
|
||||
|
||||
- Authorization gaps: routes/controllers that mutate or expose resources without a policy, gate, or middleware.
|
||||
- Insecure resource access: missing ownership checks on update/delete/show.
|
||||
- Missing policies/gates where the app has clear resource ownership.
|
||||
- Mass assignment risks: unguarded models or over-permissive `$fillable` combined with direct request input.
|
||||
- Unsafe validation assumptions: trusting `required`/client-side checks, missing server-side validation on critical fields.
|
||||
- Sensitive data exposure: secrets or personal data in responses, logs, exceptions, or dumps.
|
||||
- Unsafe configuration usage: `APP_DEBUG` assumptions, hardcoded credentials, env values echoed.
|
||||
- Dangerous file handling: unvalidated uploads, paths from user input, unsafe file reads.
|
||||
- Unsafe URL/redirect handling: open redirects from user-controlled targets.
|
||||
- Insecure authentication/authorization patterns: storing secrets in plaintext, weak session config, missing rate limits on auth.
|
||||
- Secrets accidentally committed or exposed where detectable (`.env` committed, keys in source).
|
||||
- Weak or missing CSRF protection: `VerifyCsrfToken::except` whitelists, removed/reordered CSRF middleware, forms or post endpoints missing the token.
|
||||
- Unescaped output / XSS risk: Blade `{!! !!}`, `->get()`, `->toHtml()` rendering user-controlled or stored data.
|
||||
- Raw SQL with user-controlled input: `DB::raw`, `whereRaw`, `selectRaw`, `orderByRaw`, or `statement` interpolating request input.
|
||||
- Known vulnerable dependencies. `dependencies.composer_audit` is **on by default**. Use it for `AUD-DEP-001` when `available` is true. If `available` is false, read `reason` or run `composer audit --format=json` yourself — do not treat an empty payload as “no advisories”. Set `laravel-auditor.context.composer_audit` to `false` only when you need to skip the shell-out.
|
||||
- Queued jobs that serialize Eloquent models with hidden/sensitive attributes (`AUD-QUE-002`).
|
||||
|
||||
When these packages are installed, also apply their packs (`auditor:rules --applicable`): Livewire (`AUD-LW-*`), Filament (`AUD-FIL-*`), Inertia (`AUD-IN-*`), Sanctum (`AUD-API-*`).
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
Every security finding needs at least:
|
||||
|
||||
- The specific route/controller/model file and line/range.
|
||||
- The relevant middleware/policy/gate (or the absence of one).
|
||||
- What the attacker-controlled input is, when applicable.
|
||||
|
||||
## False positives
|
||||
|
||||
- Do not claim exploitability without sufficient evidence.
|
||||
- A missing policy is only a finding when the resource is user-accessible or sensitive.
|
||||
- Uncommon patterns are not automatically vulnerabilities.
|
||||
- An empty `composer_audit` payload is not evidence that there are no advisories unless the check actually ran.
|
||||
|
||||
## Severity guidance
|
||||
|
||||
- `critical`: directly exploitable, e.g. unauthenticated access to sensitive data or arbitrary code execution.
|
||||
- `high`: requires a real but plausible precondition (e.g. any authenticated user).
|
||||
- `medium`/`low`: defense-in-depth issues, configuration concerns, or low-exploitability risks.
|
||||
- `info`: observations worth documenting without immediate risk.
|
||||
50
.agents/skills/laravel-audit-testing/SKILL.md
Normal file
50
.agents/skills/laravel-audit-testing/SKILL.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
name: laravel-audit-testing
|
||||
description: >
|
||||
Audit a Laravel application's test suite: meaningful coverage, authorization
|
||||
tests, edge cases, and Pest/PHPUnit conventions. Use when auditing testing.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Testing Audit
|
||||
|
||||
Audit the test suite of the Laravel application. **Do not equate line coverage with test quality.**
|
||||
|
||||
List applicable rules first:
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules --domain=testing --applicable
|
||||
```
|
||||
|
||||
## What to look for
|
||||
|
||||
- Critical business flows with little or no test coverage: purchases, authorization boundaries, data mutations, integrations.
|
||||
- Tests that do not verify meaningful behavior: tests that only assert the request succeeded, tests that pin implementation details, or tests that mock everything away.
|
||||
- Missing authorization/security tests around critical boundaries: no test proving an unauthorized user is rejected.
|
||||
- Important edge cases left untested: boundary values, empty states, duplicates, permissions variations.
|
||||
- Brittle testing patterns: tests coupled to exact query counts, implementation ordering, or unrelated details.
|
||||
- Excessive implementation-detail testing: tests that break on any refactor without protecting behavior.
|
||||
- Inconsistent use of Pest/PHPUnit conventions across the suite.
|
||||
- In a Pest-first suite (`tests/Pest.php` or `pestphp/pest`), new PHPUnit class tests without a documented reason (`AUD-PEST-001`).
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
Every testing finding needs at least:
|
||||
|
||||
- The critical flow or boundary (file + route/controller/service).
|
||||
- The relevant test files that should cover it.
|
||||
- What behavior is or is not asserted.
|
||||
|
||||
## False positives
|
||||
|
||||
- Coverage may exist under a different name or location — check before reporting.
|
||||
- A missing test for an unimportant flow is not a finding.
|
||||
- Do not report line-coverage numbers as proof of quality (or lack of it).
|
||||
|
||||
## Severity guidance
|
||||
|
||||
- `high`: a critical boundary (e.g. authorization) has no meaningful tests.
|
||||
- `medium`: an important flow lacks meaningful coverage or tests are brittle.
|
||||
- `low`: testing hygiene issues.
|
||||
- `info`: observations.
|
||||
134
.agents/skills/laravel-audit/SKILL.md
Normal file
134
.agents/skills/laravel-audit/SKILL.md
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
---
|
||||
name: laravel-audit
|
||||
description: >
|
||||
Perform a deep, structured, evidence-based audit of a Laravel application
|
||||
and produce actionable findings backed by concrete project evidence. Use when
|
||||
asked to audit, review, assess, or evaluate an existing Laravel codebase.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Audit
|
||||
|
||||
Perform a deep, structured, evidence-based audit of the current Laravel application. Your goal is **trustworthy findings backed by concrete evidence**, not a long list of style nitpicks.
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when asked to audit, review, assess, or evaluate an existing Laravel application for security, performance, architecture, database, testing, or Laravel conventions issues.
|
||||
|
||||
For a bounded data-structure / ownership / organizing-model pass (inventory every subsystem, fan out read-only workers, validate and rank P0–P3), use the `laravel-audit-dsa` skill instead.
|
||||
|
||||
## Core principles
|
||||
|
||||
- **Evidence-first.** Every meaningful finding must cite concrete, verifiable project context: a file path, line/range, symbol, route, migration, query, configuration key, or dependency.
|
||||
- **Deterministic facts over guesses.** Prefer programmatically gathered facts (Artisan/MCP context tools) over reading raw text and inferring.
|
||||
- **No invented behavior.** Never claim a package or framework behaves a certain way without verifying it. When uncertain, say the evidence is incomplete.
|
||||
- **Confirmed vs. hypothesis.** Label findings by confidence: `confirmed` only when evidence fully supports it; otherwise `high`, `medium`, or `low`.
|
||||
- **Trustworthiness over volume.** A few high-quality findings beat dozens of speculative ones.
|
||||
- **Read-only.** Never modify application code during the audit.
|
||||
- **No severity inflation.** Style preferences are never high severity. "Package is old" is not a finding by itself.
|
||||
|
||||
## Audit workflow
|
||||
|
||||
### Phase A: Discover
|
||||
|
||||
Gather deterministic project facts first. When Artisan is available, start with `php artisan auditor:status`, `php artisan auditor:context --list`, and `php artisan auditor:rules --applicable`. Then run the Laravel Auditor context tools (MCP) or inspect:
|
||||
|
||||
- `project_info`: Laravel version, PHP version, database engine, ecosystem packages, architecture signals.
|
||||
- `routes`: registered routes and their handlers.
|
||||
- `models`: models, tables, fillable/guarded, casts, relationships.
|
||||
- `database_schema`: tables, columns, types, indexes (read-only).
|
||||
- `dependencies`: composer packages and versions.
|
||||
- `configuration`: config keys and safe values.
|
||||
- `policies_authorization`: gates, policies, middleware.
|
||||
- `jobs_events_schedules`: jobs, events/listeners, schedules.
|
||||
- `tests`: test framework and coverage signals.
|
||||
- `migrations`: migration files.
|
||||
- `subsystems`: ownership-bounded inventory for a DSA-style coordinator audit.
|
||||
|
||||
Four tools accept optional read-only filters for focused verification: `routes` (`uri`, `name`, `action`, `method`), `models` (`class`, `table`), `database_schema` (`table`), and `dependencies` (`package`). Filtered responses report `total_count` so you always know how much of the full inventory was returned; call without arguments for the complete payload.
|
||||
|
||||
Fall back to `composer.json`, `bootstrap/app.php`, `config/app.php`, and the file tree when tools are unavailable.
|
||||
|
||||
Record the application type (web, API, admin panel, package) and any ecosystem packages (Livewire, Filament, Inertia, Pest, Sanctum, Horizon, etc.) — these determine which rules apply.
|
||||
|
||||
Build a **feature inventory** from the route surface and UI entry points before scoping. List each feature (login, checkout, admin dashboard, etc.), the routes and views that implement it, and the model/service behind it. This inventory drives testing and authorization coverage later, and surfaces stubbed or hallucinated features (a route pointing at a missing controller, an empty view, a TODO handler) early.
|
||||
|
||||
### Phase B: Scope
|
||||
|
||||
Select only the audit domains relevant to this application. Do not blindly run every check. Reason about which domains matter and state the scope before investigating.
|
||||
|
||||
Default domains: `security`, `performance`, `architecture`, `database`, `testing`, `conventions`. Skip domains that are clearly irrelevant (e.g. skip queue analysis when the app has no jobs or queue driver).
|
||||
|
||||
### Phase C: Investigate
|
||||
|
||||
For each selected domain:
|
||||
|
||||
1. Collect relevant project facts using the context tools.
|
||||
2. Inspect source code around the facts.
|
||||
3. Trace important behavior across files (controller → service → model → route → view).
|
||||
4. Cross-check evidence across multiple sources.
|
||||
5. Separate confirmed problems from hypotheses.
|
||||
|
||||
The rule reference (`php artisan auditor:rules`, or `--applicable` / `--domain=`) contains the rule IDs, severity, evidence requirements, and false-positive considerations. Use it to map observations to stable rule IDs. Ecosystem packs (Livewire, Filament, Inertia, Sanctum, Pest) only apply when those packages are installed. Queue and DSA rules always apply.
|
||||
|
||||
### Phase D: Verify
|
||||
|
||||
Before reporting a high-severity finding, attempt to verify it:
|
||||
|
||||
- Inspect the route and its middleware.
|
||||
- Inspect the model/relationship definitions.
|
||||
- Inspect the schema or migrations.
|
||||
- Inspect the config for the relevant keys.
|
||||
- Inspect dependency versions and their docs.
|
||||
- Inspect the tests.
|
||||
- Review logs/errors when relevant.
|
||||
- Run a **safe, read-only** runtime check only when a tool exists (never migrate, seed, refresh, or otherwise mutate data).
|
||||
|
||||
If you cannot verify, lower the confidence and say so explicitly.
|
||||
|
||||
### Phase E: Report
|
||||
|
||||
Write findings to `storage/auditor-findings.json` (an array, or `{ "findings": [ ... ] }`). Each finding must include:
|
||||
|
||||
- `id`: unique instance id (e.g. `F-2026-0001`).
|
||||
- `rule_id`: a stable rule ID (e.g. `AUD-SEC-001`) when one matches.
|
||||
- `title`: short and specific.
|
||||
- `domain`: the audit domain.
|
||||
- `severity`: `critical`, `high`, `medium`, `low`, or `info`.
|
||||
- `confidence`: `confirmed`, `high`, `medium`, or `low`.
|
||||
- `status`: `open` for new findings.
|
||||
- `summary`: what is wrong.
|
||||
- `why_it_matters`: why it matters for this app.
|
||||
- `evidence`: concrete references (file paths, lines, routes, symbols).
|
||||
- `affected_resources`: files/routes/config involved.
|
||||
- `recommendation`: what to do about it.
|
||||
- `remediation`: optional step-by-step guidance.
|
||||
- `verification_notes`: how you verified (or why you could not).
|
||||
- `metadata.priority`: `p0`–`p3` when you rank the report. Keep P3 small.
|
||||
- **P0** — reachable wrong-record, lost-update, authorization, or durable-state risk
|
||||
- **P1** — concrete boundary failures with less immediate damage
|
||||
- **P2** — useful invariant improvements with narrower impact
|
||||
- **P3** — telemetry / diagnostics / maintainability
|
||||
|
||||
Then render:
|
||||
|
||||
```bash
|
||||
php artisan auditor:report --findings=storage/auditor-findings.json
|
||||
php artisan auditor:ci --findings=storage/auditor-findings.json --fail-on=high
|
||||
```
|
||||
|
||||
The report includes project facts, domains audited, counts by severity/domain, priority synthesis, and the key risks.
|
||||
|
||||
## Anti-patterns to avoid
|
||||
|
||||
- Inventing vulnerabilities because a pattern is uncommon.
|
||||
- Treating every loop or every query as a performance problem.
|
||||
- Recommending "create a repository for every model" or "move everything into services".
|
||||
- Equating line coverage with test quality.
|
||||
- Claiming exploitability without evidence.
|
||||
- Recommending upgrades solely because a package is old.
|
||||
- Presenting style preferences as high-severity findings.
|
||||
- Claiming the audit is complete when evidence was missing.
|
||||
- Running `migrate`, `db:wipe`, `db:seed`, or any other mutating Artisan command to "verify" schema.
|
||||
- Treating `composer_audit.available: false` (or an empty advisory list) as “no vulnerabilities” when the collector did not actually run `composer audit`.
|
||||
136
.agents/skills/laravel-auditor-development/SKILL.md
Normal file
136
.agents/skills/laravel-auditor-development/SKILL.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
name: laravel-auditor-development
|
||||
description: >
|
||||
Integrate and apply the Laravel Auditor package in Laravel applications:
|
||||
installation, setup, the MCP tools, and the audit workflow. Use when setting
|
||||
up Laravel Auditor or wiring it into a Laravel app.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Auditor Development
|
||||
|
||||
Use this skill when a Laravel application needs to install, configure, or apply the Laravel Auditor package. Auditor does not scan the app. After setup, the user asks their AI agent to use the `laravel-audit` skill.
|
||||
|
||||
## Primary goals
|
||||
|
||||
- Install Laravel Auditor as a development dependency.
|
||||
- Run the appropriate setup command (Boost-based or standalone).
|
||||
- Verify setup with the diagnostics command.
|
||||
- Use the audit workflow skill to perform an audit.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Install the package
|
||||
|
||||
```bash
|
||||
composer require --dev mrpunyapal/laravel-auditor
|
||||
```
|
||||
|
||||
### 2. Set up resources
|
||||
|
||||
When Laravel Boost is installed, expose Auditor resources through Boost:
|
||||
|
||||
```bash
|
||||
php artisan boost:install
|
||||
```
|
||||
|
||||
(or `php artisan boost:update` after package updates).
|
||||
|
||||
When Boost is not installed, use the standalone installer:
|
||||
|
||||
```bash
|
||||
php artisan auditor:install --agents=claude_code
|
||||
php artisan auditor:install --agents=claude_code,opencode
|
||||
```
|
||||
|
||||
The installer is idempotent and safe. It detects the Laravel application context, detects whether Boost is installed, prepares Auditor's agent-facing resources, and reports what it created or updated.
|
||||
|
||||
Non-interactive runs resolve agents from `--agents`, then `laravel-auditor.agents`, then project markers. When none of those resolve, no agents are wired. A `.github` directory alone is not treated as Copilot. Pass `--force` to refresh Auditor-owned resources, including an existing `laravel-auditor` MCP entry.
|
||||
|
||||
### 3. Verify setup
|
||||
|
||||
```bash
|
||||
php artisan auditor:status
|
||||
```
|
||||
|
||||
### 4. Inspect the available rules and project facts
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules
|
||||
php artisan auditor:context --list
|
||||
php artisan auditor:context project_info
|
||||
```
|
||||
|
||||
### 5. Run an audit
|
||||
|
||||
Ask the agent to use the `laravel-audit` skill to perform a structured, evidence-based audit of the application.
|
||||
|
||||
### 6. Render a report
|
||||
|
||||
If the agent produced a JSON findings file:
|
||||
|
||||
```bash
|
||||
php artisan auditor:report --example
|
||||
php artisan auditor:report --findings=storage/auditor-findings.json --format=markdown
|
||||
```
|
||||
|
||||
Supported formats: `markdown`, `json`, `text`, `sarif`. Finding schema: publish tag `laravel-auditor-schema`.
|
||||
|
||||
### 7. Optional MCP
|
||||
|
||||
```bash
|
||||
php artisan auditor:mcp -q
|
||||
```
|
||||
|
||||
Register that stdio command with the agent so it can call `project_info`, `routes`, `models`, `migrations`, `database_schema`, `dependencies`, `configuration`, `policies_authorization`, `jobs_events_schedules`, `tests`, and `subsystems`. Use `-q` so Artisan boot output cannot break MCP stdio framing.
|
||||
|
||||
With Laravel Boost installed, the same context tools are registered automatically inside Boost's MCP server (via `boost.mcp.tools.include`); no extra setup is needed.
|
||||
|
||||
## References
|
||||
|
||||
- Install: `composer require --dev mrpunyapal/laravel-auditor`
|
||||
- Boost path: `php artisan boost:install` / `php artisan boost:update`
|
||||
- Standalone path: `php artisan auditor:install` (`--dry-run`, `--force`, `--agents=`)
|
||||
- Diagnostics: `php artisan auditor:status`
|
||||
- Rules: `php artisan auditor:rules` (`--domain=`, `--json`, `--applicable`)
|
||||
- Context: `php artisan auditor:context` (`--list`, `{collector}`, `--output=`)
|
||||
- Reports: `php artisan auditor:report` (`--findings=`, `--example`, `--format=markdown|json|text|sarif`, `--output=`)
|
||||
- CI: `php artisan auditor:ci --findings=storage/auditor-findings.json --fail-on=high`
|
||||
- Facade: `LaravelAuditor::collect('routes')`, `LaravelAuditor::rules()`, `LaravelAuditor::context()`, `LaravelAuditor::project()`
|
||||
- Config: `resources_target`, `agents`, `context.composer_audit` (on), `context.test_listing` (off)
|
||||
- Config publish tag: `laravel-auditor-config`
|
||||
- Resource publish tag: `laravel-auditor-resources`
|
||||
- Schema publish tag: `laravel-auditor-schema`
|
||||
- Example publish tag: `laravel-auditor-examples`
|
||||
- Audit workflow skill: `laravel-audit` (`laravel-audit-dsa` for subsystem / P0–P3 ranking)
|
||||
|
||||
## Examples
|
||||
|
||||
Ask the agent:
|
||||
|
||||
> Use the laravel-audit skill. Discover this Laravel app, scope the relevant domains, and report only findings with file, route, or schema evidence.
|
||||
|
||||
A finding should include `rule_id`, `severity`, `confidence`, `summary`, `why_it_matters`, `evidence`, and `recommendation`.
|
||||
|
||||
Full audit prompt:
|
||||
|
||||
> You are auditing the Laravel application in this project using the Laravel Auditor methodology.
|
||||
>
|
||||
> 1. Use the laravel-audit skill. Follow its Discover → Scope → Verify → Report workflow.
|
||||
> 2. Start by calling the context MCP tools to gather deterministic facts BEFORE reading code: `project_info`, `routes`, `models`, `migrations`, `database_schema`, `dependencies`, `configuration`, `policies_authorization`, `jobs_events_schedules`, `tests`, `subsystems`.
|
||||
> 3. Scope the relevant domains (security, database, architecture, testing, ...). Pick the domains with the most risk signal and go deep.
|
||||
> 4. For every potential finding, verify against actual files, routes, or schema. Never report a guess.
|
||||
> 5. Report findings ranked P0–P3, each with: file/route/schema evidence, the rule violated, why it matters, and a concrete fix.
|
||||
> 6. Be read-only. Do not modify any application code.
|
||||
|
||||
Discover-only quick pass:
|
||||
|
||||
> Start with a Discover phase only: run all 11 context tools, summarize what this app is (framework versions, database, route surface, model list, test coverage), and flag any immediate red flags in 3-5 bullets. Do not write findings yet.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Do not require Boost just to use Auditor.
|
||||
- Do not run `php artisan auditor:install` when Boost is installed unless a standalone path is explicitly wanted.
|
||||
- Do not modify application code during setup or during an audit.
|
||||
- Do not treat Auditor as an autonomous scanner that invents findings without evidence.
|
||||
59
.agents/skills/laravel-best-practices/SKILL.md
Normal file
59
.agents/skills/laravel-best-practices/SKILL.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
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, organized as an index of rule files. Each rule file 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, and 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.
|
||||
|
||||
## How to Apply
|
||||
|
||||
1. Check the changed files, nearby code, project configuration, and relevant tests for established patterns. Deviate only for a correctness or security defect, and call the deviation out.
|
||||
2. Map every affected concern to the rule index below. Read each mapped rule file before editing. Skip unrelated rule files.
|
||||
3. Make the smallest coherent change. Keep the application's architecture and naming instead of introducing a second pattern for the same job.
|
||||
4. Verify version-sensitive Laravel APIs for the installed version with `search-docs`, or inspect the installed framework when it is unavailable.
|
||||
5. Run the narrowest relevant tests first, then the project's formatting and static-analysis checks when the change warrants them.
|
||||
6. Re-read the diff against every mapped rule before finishing.
|
||||
|
||||
## Rule Index
|
||||
|
||||
Cross-cutting changes often need more than one rule file.
|
||||
|
||||
| Concern | Read |
|
||||
| --- | --- |
|
||||
| Query count, eager loading, indexes, large datasets | [`rules/db-performance.md`](rules/db-performance.md) |
|
||||
| Subqueries, aggregates, complex ordering and query plans | [`rules/advanced-queries.md`](rules/advanced-queries.md) |
|
||||
| Models, relationships, scopes, casts | [`rules/eloquent.md`](rules/eloquent.md) |
|
||||
| Authentication, authorization, input safety, secrets, uploads | [`rules/security.md`](rules/security.md) |
|
||||
| Form Requests and validation rules | [`rules/validation.md`](rules/validation.md) |
|
||||
| Controllers, route binding, resources, middleware | [`rules/routing.md`](rules/routing.md) |
|
||||
| Schema changes, columns, foreign keys, indexes | [`rules/migrations.md`](rules/migrations.md) |
|
||||
| Jobs, retries, uniqueness, batches, Horizon | [`rules/queue-jobs.md`](rules/queue-jobs.md) |
|
||||
| Cache lifetime, invalidation, locks, memoization | [`rules/caching.md`](rules/caching.md) |
|
||||
| Outbound requests, retries, timeouts, fakes | [`rules/http-client.md`](rules/http-client.md) |
|
||||
| Exceptions, reporting, rendering, log context | [`rules/error-handling.md`](rules/error-handling.md) |
|
||||
| Events and notifications | [`rules/events-notifications.md`](rules/events-notifications.md) |
|
||||
| Mailables and mail assertions | [`rules/mail.md`](rules/mail.md) |
|
||||
| Scheduled tasks and overlap protection | [`rules/scheduling.md`](rules/scheduling.md) |
|
||||
| Collections, lazy iteration, bulk operations | [`rules/collections.md`](rules/collections.md) |
|
||||
| Blade components, attributes, composers | [`rules/blade-views.md`](rules/blade-views.md) |
|
||||
| Environment values and application configuration | [`rules/config.md`](rules/config.md) |
|
||||
| Tests: coverage, factories, fakes, and assertions | the `testing-best-practices` skill |
|
||||
| Naming, helpers, file boundaries, PHP style | [`rules/style.md`](rules/style.md) |
|
||||
| Actions, services, dependencies, application structure | [`rules/architecture.md`](rules/architecture.md) |
|
||||
|
||||
## Decision Rules
|
||||
|
||||
- Prefer framework features and existing application abstractions over new helpers or dependencies.
|
||||
- Avoid speculative abstractions. Extract code when it creates a clear domain boundary, removes meaningful duplication, or makes behavior independently testable.
|
||||
- Keep database access out of Blade views and prevent hidden N+1 queries across controllers, resources, jobs, and serialization.
|
||||
106
.agents/skills/laravel-best-practices/rules/advanced-queries.md
Normal file
106
.agents/skills/laravel-best-practices/rules/advanced-queries.md
Normal file
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
```
|
||||
206
.agents/skills/laravel-best-practices/rules/architecture.md
Normal file
206
.agents/skills/laravel-best-practices/rules/architecture.md
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
# 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 handle(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. Without an explicit `ORDER BY`, row order is undefined.
|
||||
|
||||
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, inside a transaction
|
||||
DB::transaction(function () use ($id) {
|
||||
$product = Product::where('id', $id)->lockForUpdate()->first();
|
||||
|
||||
// Read and update the product while the lock is held...
|
||||
});
|
||||
```
|
||||
|
||||
## 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);
|
||||
}
|
||||
}
|
||||
```
|
||||
36
.agents/skills/laravel-best-practices/rules/blade-views.md
Normal file
36
.agents/skills/laravel-best-practices/rules/blade-views.md
Normal file
|
|
@ -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
|
||||
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
|
||||
{{ $message }}
|
||||
</div>
|
||||
```
|
||||
|
||||
## 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.
|
||||
70
.agents/skills/laravel-best-practices/rules/caching.md
Normal file
70
.agents/skills/laravel-best-practices/rules/caching.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Caching Best Practices
|
||||
|
||||
## Use `Cache::remember()` Instead of Manual Get/Put
|
||||
|
||||
Cleaner cache-aside pattern that removes boilerplate. use `Cache::lock()` for race conditions.
|
||||
|
||||
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. Not supported by the `file`, `dynamodb`, `database` or `storage` drivers.
|
||||
|
||||
```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']],
|
||||
```
|
||||
44
.agents/skills/laravel-best-practices/rules/collections.md
Normal file
44
.agents/skills/laravel-best-practices/rules/collections.md
Normal file
|
|
@ -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 {}
|
||||
```
|
||||
73
.agents/skills/laravel-best-practices/rules/config.md
Normal file
73
.agents/skills/laravel-best-practices/rules/config.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Configuration Best Practices
|
||||
|
||||
## `env()` Only in Config Files
|
||||
|
||||
Direct `env()` calls may 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'));
|
||||
```
|
||||
192
.agents/skills/laravel-best-practices/rules/db-performance.md
Normal file
192
.agents/skills/laravel-best-practices/rules/db-performance.md
Normal file
|
|
@ -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
|
||||
```
|
||||
150
.agents/skills/laravel-best-practices/rules/eloquent.md
Normal file
150
.agents/skills/laravel-best-practices/rules/eloquent.md
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# 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
|
||||
#[Scope]
|
||||
protected function active(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
|
||||
#[Scope]
|
||||
protected function published(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.
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# 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 — call `afterCommit()` to delay dispatch until the transaction commits.
|
||||
|
||||
```php
|
||||
$user->notify((new InvoicePaid($invoice))->afterCommit());
|
||||
```
|
||||
|
||||
## 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.
|
||||
160
.agents/skills/laravel-best-practices/rules/http-client.md
Normal file
160
.agents/skills/laravel-best-practices/rules/http-client.md
Normal file
|
|
@ -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.example.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.example.com/v1/charges', $data);
|
||||
```
|
||||
|
||||
Only retry on specific errors:
|
||||
|
||||
```php
|
||||
$response = Http::retry(3, 100, function (Throwable $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(),
|
||||
]);
|
||||
```
|
||||
27
.agents/skills/laravel-best-practices/rules/mail.md
Normal file
27
.agents/skills/laravel-best-practices/rules/mail.md
Normal file
|
|
@ -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 fail `assertSent` with a "Did you mean to use assertQueued()?" hint.
|
||||
|
||||
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.
|
||||
121
.agents/skills/laravel-best-practices/rules/migrations.md
Normal file
121
.agents/skills/laravel-best-practices/rules/migrations.md
Normal file
|
|
@ -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')->index()->constrained();
|
||||
$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']);
|
||||
```
|
||||
144
.agents/skills/laravel-best-practices/rules/queue-jobs.md
Normal file
144
.agents/skills/laravel-best-practices/rules/queue-jobs.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# 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(): \DateTimeInterface
|
||||
{
|
||||
return now()->addHours(4);
|
||||
}
|
||||
```
|
||||
|
||||
## Use `ShouldBeUniqueUntilProcessing` for Early Lock Release
|
||||
|
||||
`ShouldBeUnique` holds the lock until the job completes. `ShouldBeUniqueUntilProcessing` releases it when processing starts, allowing new instances to queue.
|
||||
|
||||
```php
|
||||
class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
|
||||
{
|
||||
// Lock releases when processing begins, not when it finishes
|
||||
}
|
||||
```
|
||||
|
||||
## 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,
|
||||
],
|
||||
],
|
||||
],
|
||||
```
|
||||
99
.agents/skills/laravel-best-practices/rules/routing.md
Normal file
99
.agents/skills/laravel-best-practices/rules/routing.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# 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);
|
||||
// In routes/api.php — the /api prefix is applied automatically
|
||||
Route::apiResource('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');
|
||||
}
|
||||
```
|
||||
39
.agents/skills/laravel-best-practices/rules/scheduling.md
Normal file
39
.agents/skills/laravel-best-practices/rules/scheduling.md
Normal file
|
|
@ -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');
|
||||
});
|
||||
```
|
||||
198
.agents/skills/laravel-best-practices/rules/security.md
Normal file
198
.agents/skills/laravel-best-practices/rules/security.md
Normal file
|
|
@ -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(UpdatePostRequest $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/PATCH/DELETE Blade forms. Inertia doesn't use `@csrf`; its HTTP client sends the `XSRF-TOKEN` cookie back as the `X-XSRF-TOKEN` header, which Laravel accepts in place of the `_token` field.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
<form method="POST" action="/posts">
|
||||
<input type="text" name="title">
|
||||
</form>
|
||||
```
|
||||
|
||||
Correct:
|
||||
```blade
|
||||
<form method="POST" action="/posts">
|
||||
@csrf
|
||||
<input type="text" name="title">
|
||||
</form>
|
||||
```
|
||||
|
||||
## 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 and size. Both `mimes` and `mimetypes` read the file's contents to guess its MIME type; `mimes` just expresses the allow-list as extensions. The `extensions` rule checks only the client-supplied filename, so never rely on it alone. 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
125
.agents/skills/laravel-best-practices/rules/style.md
Normal file
125
.agents/skills/laravel-best-practices/rules/style.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Conventions & Style
|
||||
|
||||
## Follow Laravel Naming Conventions
|
||||
|
||||
| What | Convention | Good | Bad |
|
||||
|------|-----------|------|-----|
|
||||
| Controller | singular | `ArticleController` | `ArticlesController` |
|
||||
| Model | singular | `User` | `Users` |
|
||||
| Table | plural, snake_case | `article_comments` | `articleComments` |
|
||||
| Pivot table | singular alphabetical | `article_user` | `user_article` |
|
||||
| Column | snake_case, no model name | `meta_title` | `article_meta_title` |
|
||||
| Foreign key | singular model + `_id` | `article_id` | `articles_id` |
|
||||
| Route | plural | `articles/1` | `article/1` |
|
||||
| Route name | snake_case with dots | `users.show_active` | `users.show-active` |
|
||||
| Method | camelCase | `getAll` | `get_all` |
|
||||
| Variable | camelCase | `$articlesWithAuthor` | `$articles_with_author` |
|
||||
| Collection | descriptive, plural | `$activeUsers` | `$data` |
|
||||
| Object | descriptive, singular | `$activeUser` | `$users` |
|
||||
| View | kebab-case | `show-filtered.blade.php` | `showFiltered.blade.php` |
|
||||
| Config | snake_case | `google_calendar.php` | `googleCalendar.php` |
|
||||
| Enum | singular | `UserType` | `UserTypes` |
|
||||
|
||||
## Prefer Shorter Readable Syntax
|
||||
|
||||
| Verbose | Shorter |
|
||||
|---------|---------|
|
||||
| `Session::get('cart')` | `session('cart')` |
|
||||
| `$request->session()->get('cart')` | `session('cart')` |
|
||||
| `$request->input('name')` | `$request->name` |
|
||||
| `return Redirect::back()` | `return back()` |
|
||||
| `Carbon::now()` | `now()` |
|
||||
| `App::make('Class')` | `app('Class')` |
|
||||
| `->where('column', '=', 1)` | `->where('column', 1)` |
|
||||
| `->orderBy('created_at', 'desc')` | `->latest()` |
|
||||
| `->orderBy('created_at', 'asc')` | `->oldest()` |
|
||||
| `->first()->name` | `->value('name')` |
|
||||
|
||||
## Use Laravel String & Array Helpers
|
||||
|
||||
Laravel provides `Str`, `Arr`, `Number`, and `Uri` helper classes that are more readable, chainable, and UTF-8 safe than raw PHP functions. Always prefer them.
|
||||
|
||||
Strings — use `Str` and fluent `Str::of()` over raw PHP:
|
||||
```php
|
||||
// Incorrect
|
||||
$slug = strtolower(str_replace(' ', '-', $title));
|
||||
$short = substr($text, 0, 100) . '...';
|
||||
$class = substr(strrchr('App\Models\User', '\\'), 1);
|
||||
|
||||
// Correct
|
||||
$slug = Str::slug($title);
|
||||
$short = Str::limit($text, 100);
|
||||
$class = class_basename('App\Models\User');
|
||||
```
|
||||
|
||||
Fluent strings — chain operations for complex transformations:
|
||||
```php
|
||||
// Incorrect
|
||||
$result = strtolower(trim(str_replace('_', '-', $input)));
|
||||
|
||||
// Correct
|
||||
$result = Str::of($input)->trim()->replace('_', '-')->lower();
|
||||
```
|
||||
|
||||
Key `Str` methods to prefer: `Str::slug()`, `Str::limit()`, `Str::contains()`, `Str::before()`, `Str::after()`, `Str::between()`, `Str::camel()`, `Str::snake()`, `Str::kebab()`, `Str::headline()`, `Str::squish()`, `Str::mask()`, `Str::uuid()`, `Str::ulid()`, `Str::random()`, `Str::is()`.
|
||||
|
||||
Arrays — use `Arr` over raw PHP:
|
||||
```php
|
||||
// Incorrect
|
||||
$name = isset($array['user']['name']) ? $array['user']['name'] : 'default';
|
||||
|
||||
// Correct
|
||||
$name = Arr::get($array, 'user.name', 'default');
|
||||
```
|
||||
|
||||
Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`.
|
||||
|
||||
Numbers — use `Number` for display formatting:
|
||||
```php
|
||||
Number::format(1000000); // "1,000,000"
|
||||
Number::currency(1500, 'USD'); // "$1,500.00"
|
||||
Number::abbreviate(1000000); // "1M"
|
||||
Number::fileSize(1024 * 1024); // "1 MB"
|
||||
Number::percentage(75.5); // "75.5%"
|
||||
```
|
||||
|
||||
URIs — use `Uri` for URL manipulation:
|
||||
```php
|
||||
$uri = Uri::of('https://example.com/search')
|
||||
->withQuery(['q' => 'laravel', 'page' => 1]);
|
||||
```
|
||||
|
||||
Use `$request->string('name')` to get a fluent `Stringable` directly from request input for immediate chaining.
|
||||
|
||||
Use `search-docs` for the full list of available methods — these helpers are extensive.
|
||||
|
||||
## No Inline JS/CSS in Blade
|
||||
|
||||
Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
let article = `{{ json_encode($article) }}`;
|
||||
```
|
||||
|
||||
Correct:
|
||||
```blade
|
||||
<button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}</button>
|
||||
```
|
||||
|
||||
Pass data to JS via data attributes or use a dedicated PHP-to-JS package.
|
||||
|
||||
## No Unnecessary Comments
|
||||
|
||||
Code should be readable on its own. Use descriptive method and variable names instead of comments. The only exception is config files, where descriptive comments are expected.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
// Check if there are any joins
|
||||
if (count((array) $builder->getQuery()->joins) > 0)
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
if ($this->hasJoins())
|
||||
```
|
||||
75
.agents/skills/laravel-best-practices/rules/validation.md
Normal file
75
.agents/skills/laravel-best-practices/rules/validation.md
Normal file
|
|
@ -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.');
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
65
.agents/skills/testing-best-practices/SKILL.md
Normal file
65
.agents/skills/testing-best-practices/SKILL.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
name: testing-best-practices
|
||||
description: "Laravel test design and review. Use when selecting coverage, naming or structuring tests, choosing assertions or test data, isolating dependencies, testing HTTP or security boundaries, improving suite performance, or reviewing test value. Use framework guidance or search-docs for Pest and PHPUnit syntax."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Testing Best Practices
|
||||
|
||||
This skill provides rules for designing Laravel tests. Each rule file explains what to do and why. Use `search-docs` for Laravel and Pest API syntax.
|
||||
This project uses Pest. Follow the corresponding guidance in each rule.
|
||||
|
||||
## Consistency First
|
||||
|
||||
Read nearby tests before you choose syntax and organization.
|
||||
|
||||
A pattern repeated throughout the project is a convention, and project conventions take precedence over this skill. Follow them and give new tests the same structure.
|
||||
|
||||
These rules govern the tests you write now. An existing test that follows a project convention is not defective merely because it conflicts with this skill. Do not delete or rewrite it. If the convention has drawbacks, explain them and let the user decide.
|
||||
|
||||
Use the project convention for each item that follows:
|
||||
|
||||
- the use of `it()` or `test()`
|
||||
- the construction of a factory
|
||||
- the setup of the authentication
|
||||
- the layout of the files
|
||||
|
||||
## What to Test
|
||||
|
||||
Read this section before you write a test.
|
||||
|
||||
- Test observable behavior and application contracts. A test must pass after an implementation change if the behavior stays the same.
|
||||
- Cover every changed decision and each applicable high-value failure mode. A decision is a branch, a validation, a calculation, or an authorization.
|
||||
- Exercise declarations through behavior instead of repeating their text.
|
||||
- Leave framework behavior to framework tests. Testing project configuration is not testing the framework. A constrained relationship, cast, scope, or validation rule belongs to this project.
|
||||
- Keep every test that can detect a distinct defect. When two tests detect the same defect, trim the higher-layer test to one case and report the duplication. Do not delete an existing test.
|
||||
- Write a feature test first. Write a unit test only for logic that does not use the framework.
|
||||
- Write a feature test for every behavior reachable through a request. Real-browser tests require `pestphp/pest-plugin-browser` and a browser download, neither of which this project installs. Mention the package only if the user asks for a real-browser test.
|
||||
- Judge an architecture test by the convention it protects, not by the rules above. An `arch()` test declares a rule for an entire directory, such as the parent class of every model, the classes that may use an enum, or the methods every factory declares. It intentionally checks declarations and fails when a new file breaks the convention.
|
||||
- Use the test tools that the project installs. Add a new test dependency, plugin, or browser only after the user asks for it.
|
||||
|
||||
## How to Apply
|
||||
|
||||
1. Read the code under test. Read the tests in the same directory. Identify every decision in the code.
|
||||
2. Select every applicable branch in the rule index. Read every selected rule file.
|
||||
3. Report each defect in the code before you write a test. Examples are a method with no body, a policy that no action calls, and a write action with no validation. Test the actual behavior. Report the defect to the user.
|
||||
4. Write the tests. Run the smallest set of tests that covers the change. The tests must pass.
|
||||
5. Check every applicable item in `rules/review.md` and every selected rule file. Resolve every mismatch before completion.
|
||||
|
||||
## Rule Index
|
||||
|
||||
Most changes need more than one rule file.
|
||||
|
||||
| Subject | Rule file |
|
||||
| --- | --- |
|
||||
| A feature of the test framework that can already do the work | [`rules/finding-features.md`](rules/finding-features.md) |
|
||||
| The layout of the files, the names of the tests, and the groups | [`rules/naming.md`](rules/naming.md) |
|
||||
| Arrange-act-assert, and the correct assertion for each subject | [`rules/assertions.md`](rules/assertions.md) |
|
||||
| The coverage of an endpoint, the authentication, the authorization, the isolation of a tenant, the validation, and the tests in a browser | [`rules/endpoint-tests.md`](rules/endpoint-tests.md) |
|
||||
| The factories, the owner of the test data, and the repeated input values | [`rules/test-data.md`](rules/test-data.md) |
|
||||
| The fakes, the mocks, the outbound HTTP, the time, the randomness, and the database | [`rules/isolation.md`](rules/isolation.md) |
|
||||
| The escaping, the injection, the access across tenants, and the checks of privilege | [`rules/security.md`](rules/security.md) |
|
||||
| The settings of the environment and of the CI for a slow suite | [`rules/performance.md`](rules/performance.md) |
|
||||
| The review of a test or of a suite | [`rules/review.md`](rules/review.md) |
|
||||
60
.agents/skills/testing-best-practices/rules/assertions.md
Normal file
60
.agents/skills/testing-best-practices/rules/assertions.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Assertions
|
||||
|
||||
## Arrange, Act, Assert
|
||||
|
||||
Write each test in three parts: setup, one action, and assertions. Put one blank line between them so readers can identify each part without comments.
|
||||
|
||||
Keep each test self-contained. Do not use values created by another test.
|
||||
|
||||
## How to Find the Correct Assertion
|
||||
|
||||
First identify the subject of the check, then find an assertion designed for it. A subject-specific assertion identifies the incorrect value when the test fails.
|
||||
|
||||
1. Search Laravel's assertions for framework subjects such as responses, the database, sessions, models, queues, events, mail, and notifications.
|
||||
2. Fetch `https://pestphp.com/docs/expectations.md` for the expectations of Pest for a plain value, a type, a format, or a shape.
|
||||
3. Build the check by hand only if no assertion exists for the subject.
|
||||
4. Confirm the name in the documentation before you use it. Do not write an assertion that you did not confirm.
|
||||
|
||||
Use the assertion in this table for each subject.
|
||||
|
||||
| Subject | Assertion to use |
|
||||
| --- | --- |
|
||||
| A return value, the state of an object, or a transformation of a value | an `expect()` chain |
|
||||
| An HTTP status, JSON, a session, or Inertia | a Laravel response assertion |
|
||||
| The state in the database | a Laravel database assertion |
|
||||
| The existence of a model | `assertModelExists($model)` rather than `assertDatabaseHas('users', ['id' => $user->id])` |
|
||||
|
||||
Use a PHPUnit assertion only if no Pest expectation and no Laravel assertion exists for the subject.
|
||||
|
||||
Assert each fact once. Do not assert a 200 status before `assertSee`, because `assertSee` already shows that the page rendered.
|
||||
|
||||
## The Assertion with a Name for a Response
|
||||
|
||||
Use a named response assertion, such as `assertNotFound()`, rather than `assertStatus(404)`. A failure then identifies the broken contract. Laravel provides named assertions for commonly tested status codes.
|
||||
|
||||
Keep one `expect()` chain on one subject. Start a new chain when the subject changes, or when the chain is difficult to read.
|
||||
|
||||
## Assert a Known Value
|
||||
|
||||
Write the expected value in the test, or calculate the expected value by a different method. Do not calculate the expected value with the logic of the implementation, because the test then passes when that logic is wrong.
|
||||
|
||||
```php
|
||||
// The test calculates the value with the logic of the implementation.
|
||||
$expected = now()->subHours(24)->floorSeconds(30)->toJson();
|
||||
expect($from)->toBe($expected);
|
||||
|
||||
// The test sets a fixed input and asserts a known value.
|
||||
travelTo('2025-01-01 00:00:00');
|
||||
expect($from)->toBe('2024-12-31T00:00:00.000000Z');
|
||||
```
|
||||
|
||||
## Assert the Complete Result
|
||||
|
||||
A status code is not the complete result of a write operation. Assert each of the following if the operation changes it:
|
||||
|
||||
- the response or the return value
|
||||
- the state in the database
|
||||
- the jobs and the events that the operation dispatches
|
||||
- the notifications and the mail that the operation sends
|
||||
|
||||
On the failure path, assert that the operation makes none of these changes. A test that asserts only `assertOk()` passes even when the application saves no record.
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# Endpoint Tests
|
||||
|
||||
## How to Write the Test
|
||||
|
||||
Fetch `https://laravel.com/framework/docs/http-tests` for the request helpers, the authentication helpers, and the response assertions. Confirm the name before you use it, and do not guess an assertion.
|
||||
|
||||
Choose an assertion based on the subject of the check: the status, a header, a redirect, the JSON body, the session, a validation error, or the view. Laravel provides a named assertion for each subject that identifies the incorrect value.
|
||||
|
||||
## The Coverage of an Endpoint
|
||||
|
||||
Write a test for each applicable case:
|
||||
|
||||
- The request has missing or invalid authentication.
|
||||
- The request comes from a different tenant, team, or organization.
|
||||
- The user has an insufficient role or permission.
|
||||
- The request does not satisfy a route or scope constraint.
|
||||
- The request fails the validation.
|
||||
- The request is valid. Assert both the response and the persisted state.
|
||||
|
||||
Assert the application's actual behavior rather than a generic status code. An API returns `401` for a missing or invalid token, while a browser endpoint redirects to the sign-in route.
|
||||
|
||||
## The Isolation of a Tenant
|
||||
|
||||
Assert the status code returned for a cross-tenant request. Use `404` rather than `403` when one tenant must not learn that another tenant's record exists, because `403` confirms its existence.
|
||||
|
||||
## Test Authorization at the Policy Level
|
||||
|
||||
An HTTP test shows that the endpoint performs authorization. It cannot identify which mechanism refused the request because middleware, a policy, and a call to `abort()` can all return `403`.
|
||||
|
||||
- Assert the complete matrix of the permissions against the policy or the gate. A failure then names the rule that is not correct.
|
||||
- Write one HTTP test for one refused role, which shows that the endpoint calls the authorization.
|
||||
- Use the helper of the project that asserts the ability and the arguments of the gate, if such a helper exists.
|
||||
|
||||
## The Validation
|
||||
|
||||
- Write one test for each validation rule when each failure represents a separate contract.
|
||||
- Write one test with an empty payload to assert several required fields together.
|
||||
- Give the status code in the name of a test for an API.
|
||||
- Assert the text of the message that the user gets. A message that is present but wrong is a defect.
|
||||
- Use a dataset for input values that need the same setup and the same assertions.
|
||||
|
||||
Send an input value that is not valid through the application, and assert the error. Do not assert that an array of rules contains a string, because that assertion tests the declaration and not the behavior. Use such an assertion only for a rule that no request can reach, and write the reason in the test.
|
||||
|
||||
### Which Layer Owns Which Case
|
||||
|
||||
The rule-class test owns the matrix of values that pass and fail. The endpoint test proves that the endpoint applies the rule and that the user receives the message.
|
||||
|
||||
When both tests contain the matrix, move it to the rule-class test and retain one case in the endpoint test. Never remove the last case, because the rule-class test still passes if the request omits the rule. The same division applies to policies, scopes, and other classes called by a request.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# How to Find Test Framework Features
|
||||
|
||||
Pest adds features faster than this skill can list them. Find an existing feature before implementing the behavior by hand.
|
||||
|
||||
- Give `search-docs` the capability you need rather than the name of a function you remember. It returns features available in the installed version.
|
||||
- Fetch `https://pestphp.com/llms.txt` for the complete feature list and additions in each release.
|
||||
- If a search returns no results, tell the user that the installed version does not provide the feature. Do not write an API that you have not confirmed.
|
||||
|
||||
Search for a feature in this table before you write the code by hand.
|
||||
|
||||
| Work that you need | Term to search for |
|
||||
| --- | --- |
|
||||
| Run one test with many input values | datasets, bound datasets |
|
||||
| Assert over many values or over a collection | higher-order expectations |
|
||||
| Remove the same setup from each test in a file | hooks, higher-order tests |
|
||||
| Apply a convention to the complete codebase | architecture testing |
|
||||
| Measure if the suite finds a defect | mutation testing |
|
||||
| Find code with no types | type coverage |
|
||||
| Reduce the time of a slow suite | parallel, profiling |
|
||||
| Run one test while you debug | filtering, `--bail`, `--dirty` |
|
||||
|
||||
## The Assertions of Laravel
|
||||
|
||||
Laravel provides assertions for each part of the framework. Fetch `https://laravel.com/framework/docs/testing` for the complete list, and search for an assertion before building a check by hand. Examples include `assertDatabaseHas()`, `assertModelExists()`, `assertSoftDeleted()`, response assertions such as `assertRedirectToRoute()` and `assertJsonPath()`, and fake assertions such as `Queue::assertPushed()` and `Notification::assertSentTo()`.
|
||||
|
||||
A hand-built check fails with `false is not true`, which identifies nothing. A framework assertion names the incorrect table, value, or response, so the failure indicates what to fix.
|
||||
|
||||
```php
|
||||
// The failure says that false is not true.
|
||||
// Instead of this
|
||||
expect(User::where('email', 'taylor@laravel.com')->exists())->toBeTrue();
|
||||
|
||||
// Use this
|
||||
// The failure names the table and the attributes that it did not find.
|
||||
$this->assertDatabaseHas('users', ['email' => 'taylor@laravel.com']);
|
||||
```
|
||||
52
.agents/skills/testing-best-practices/rules/isolation.md
Normal file
52
.agents/skills/testing-best-practices/rules/isolation.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Fakes, Mocks, and Determinism
|
||||
|
||||
Tests that depend on actual time, randomness, sleeping, or network calls can fail for reasons unrelated to the code under test. Control all four.
|
||||
|
||||
## How to Isolate a Dependency
|
||||
|
||||
Fetch `https://laravel.com/framework/docs/mocking` for Laravel's fakes, facade doubles, and fake assertions. Confirm each name before using it.
|
||||
|
||||
Identify the dependency, then choose the first applicable option. A framework fake preserves the real code path, while a mock replaces the dependency.
|
||||
|
||||
1. Use framework fakes for facades such as events, queues, mail, notifications, storage, the HTTP client, time, and sleep.
|
||||
2. Use the fake implementation of the project for a service of the project, if such a fake exists.
|
||||
3. Use a mock for a container-resolved contract only when the real implementation leaves the process or is nondeterministic.
|
||||
4. Use the real implementation for everything else, including the database.
|
||||
|
||||
## The Fakes
|
||||
|
||||
- Create each fake inside the test that needs it. Do not create fakes in a file-level `beforeEach()`.
|
||||
- Pass class names to `Event::fake()` and `Queue::fake()` when you know which classes the code dispatches. A fake without class names can hide an unexpected dispatch.
|
||||
- Use a fake without class names only when the test asserts the complete result, including a call to `assertNothingPushed()`.
|
||||
- Write one assertion for each fake. The assertion states that the code dispatches the item, or that the code does not dispatch the item.
|
||||
- Assert the data of a job or of an event if that data is part of the behavior.
|
||||
- Use `Exceptions::fake()` to assert that the application reports the correct exception. Do not use `withoutExceptionHandling()`, because it changes the response under test.
|
||||
|
||||
Create prerequisite factory records before calling `Event::fake()`. Factories use model events, such as a `creating` hook that generates a UUID, and a fake without class names suppresses those events and can produce an invalid model. Call the fake first only when a factory event is under test, and pass that event's class name.
|
||||
|
||||
## The Mocks
|
||||
|
||||
Use `shouldReceive()` before the action to declare an expectation. Use `shouldHaveReceived()` after the action for a spy. Use `Mockery::on()` or `withArgs()` if an equality check cannot state the expected argument, such as a check of one field of a value object.
|
||||
|
||||
Import the mock function before you use it: `use function Pest\Laravel\mock;`.
|
||||
|
||||
## The Outbound HTTP
|
||||
|
||||
Call `Http::preventStrayRequests()`. Any request without a matching fake then fails without reaching the network.
|
||||
|
||||
Fake the exact endpoint used by each test. Do not call `Http::fake()` without an endpoint because it accepts unexpected requests and can hide defects.
|
||||
|
||||
## The Time and the Randomness
|
||||
|
||||
- Freeze the time or move the time in each test that depends on a date, a period, or a timestamp.
|
||||
- Use the framework helpers `freezeTime()`, `travelTo()`, `travel()`, and `travelBack()`. Do not call `Carbon::setTestNow()`.
|
||||
- Use `Str::createRandomStringsUsing()` to fix a generated string, if the test asserts an identifier or a slug.
|
||||
- Use `Sleep::fake()` instead of a real sleep, and assert the sleeps that the code requests.
|
||||
- Restore the time and the randomness after each test, if the suite does not restore them for every test.
|
||||
|
||||
## The Database
|
||||
|
||||
- Run the real query against the real records in the test database. Do not mock the query builder, because the test then asserts the mock.
|
||||
- Assert the exact keys of `toArray()` if the shape of the serialized model is a contract. The test then fails when the model exposes a new attribute.
|
||||
- Test application behavior caused by the schema, such as deleting dependent records through a cascade. Do not test the database engine's cascade implementation.
|
||||
- Use `LazilyRefreshDatabase` instead of `RefreshDatabase`. A test that does not use the database then does not run the migrations.
|
||||
45
.agents/skills/testing-best-practices/rules/naming.md
Normal file
45
.agents/skills/testing-best-practices/rules/naming.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Naming and Structure
|
||||
|
||||
## File Layout
|
||||
|
||||
- Name each test file `{ClassName}Test.php`.
|
||||
- Place each test file at the same relative path as the class under test. The class `app/Actions/DeleteTeam.php` gets the test `tests/Unit/Actions/DeleteTeamTest.php`.
|
||||
- Follow the project's convention for fixture files. If none exists, put fixtures in `tests/Fixtures/` and load them by path.
|
||||
- Move large literal values out of the test body and into fixture files.
|
||||
|
||||
## The Test Function
|
||||
|
||||
Use the test function used by other files in the same directory. If no neighboring test files exist:
|
||||
|
||||
- Use `it()` for the behavior of the code, and write the name as a verb phrase.
|
||||
- Use `test()` for a declarative fact, such as a grant in a policy, the labels of an enum, or the shape of a serialized model.
|
||||
|
||||
Use one Pest declaration style in each file. Use either `it()` or `test()` consistently.
|
||||
|
||||
## The Names of the Tests
|
||||
|
||||
The name of a test is a specification. State the user-visible result and the condition that causes it.
|
||||
|
||||
- Name the behavior, and not the method under test. The file name already gives the class.
|
||||
- Give the exact status code in the name of a test for an API error.
|
||||
- Do not write `Given`, `When`, or `Then` in the name.
|
||||
|
||||
```php
|
||||
it('returns 401 when no token is provided', function () { ... });
|
||||
it('does not include deployments from deleted environments', function () { ... });
|
||||
it('falls back to the default region when none is configured', function () { ... });
|
||||
```
|
||||
|
||||
Use a verb that describes a result, such as `returns`, `renders`, `creates`, `dispatches`, `rejects`, `forbids`, `falls back`, or `does not`.
|
||||
|
||||
Do not write `it('works correctly')` or `it('returns data')`, because neither specifies a meaningful result. Do not write `it('handleMethod creates record')`, because it names a method rather than behavior.
|
||||
|
||||
## Grouping
|
||||
|
||||
Use `describe()` if one file covers separate actions in a lifecycle. An example is a controller with the actions `index`, `show`, `store`, `update`, and `destroy`.
|
||||
|
||||
Do not use `describe()` in these cases:
|
||||
|
||||
- The file covers one action or one flow.
|
||||
- The tests are different only in the input value. Use a dataset instead.
|
||||
- The group adds a level but does not make the file easier to read.
|
||||
46
.agents/skills/testing-best-practices/rules/performance.md
Normal file
46
.agents/skills/testing-best-practices/rules/performance.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Test Suite Performance
|
||||
|
||||
These settings apply to the project and CI, not to individual tests. Read `rules/isolation.md` for choices within a test.
|
||||
|
||||
Fetch `https://pestphp.com/docs/optimizing-tests` for Pest options that make test runs faster.
|
||||
Verify each flag in the documentation before adding it to CI.
|
||||
|
||||
Measure before changing a setting. Find the slow test first, and apply a project-wide setting only after identifying the costly work.
|
||||
|
||||
## The Environment
|
||||
|
||||
- Set `BCRYPT_ROUNDS=4` in `.env.testing` or in `phpunit.xml`. The default value is 12, and the hash then takes most of the time of each test that signs a user in.
|
||||
- Disable XDebug. Disable pcov also, unless the run needs the coverage.
|
||||
- Disable packages that perform work on every request in the test environment. Examples are Pulse, Telescope, and Nightwatch.
|
||||
- Use the `WithCachedConfig` and `WithCachedRoutes` traits, so the run does not parse the configuration and the routes for every test.
|
||||
- Call `withoutVite()`, or `withoutMix()`, so the framework does not resolve a built asset.
|
||||
|
||||
## The Global Fakes
|
||||
|
||||
Put these three calls in the base `Pest.php` of the project:
|
||||
|
||||
- `Http::preventStrayRequests()`, because one request that reaches the network can slow the suite. This catches requests made through Laravel's HTTP client. Check direct Guzzle and cURL usage separately.
|
||||
- `Sleep::fake(syncWithCarbon: true)`, so a retry and a backoff do not sleep.
|
||||
- `Exceptions::fake()`, so the suite does not report an exception to an external service.
|
||||
|
||||
## How to Run the Suite in Parallel
|
||||
|
||||
Run `vendor/bin/pest --parallel` to spread tests across the machine's CPU cores. Add `--processes=N` if the default count is unsuitable for the machine or CI.
|
||||
|
||||
A parallel run gives each process a separate database. Tests must meet these conditions; a test that fails only in parallel breaks one of them:
|
||||
|
||||
- The test creates each record that it reads. It does not read a record that another test creates.
|
||||
- The test does not depend on the order of the run.
|
||||
- The test does not share a file, a cache key, or a queue with another test. Give each process a separate name for such a resource.
|
||||
|
||||
## How to Find a Slow Test
|
||||
|
||||
Run `vendor/bin/pest --profile` to list the slowest tests. Start with the ten slowest tests, because the same cause often applies to the complete suite.
|
||||
|
||||
If the cause of a slow test is unclear, add an event listener or temporary log entry to identify its work.
|
||||
|
||||
## Common Errors
|
||||
|
||||
- The run loads XDebug for a test that does not need it.
|
||||
- `BCRYPT_ROUNDS` keeps the default value, because the project has no `.env.testing`.
|
||||
- The code under test calls the real `sleep()`, and `Sleep::fake()` then does not help.
|
||||
53
.agents/skills/testing-best-practices/rules/review.md
Normal file
53
.agents/skills/testing-best-practices/rules/review.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Reviewing Tests
|
||||
|
||||
Check every item in this file. A passing test may still provide no value. For each test, identify the defect it would catch.
|
||||
|
||||
Report each finding. Do not delete or rewrite a test without the user's approval. When an issue appears throughout the suite as a convention, report the pattern once rather than every affected file.
|
||||
|
||||
## The Value of the Test
|
||||
|
||||
Apply this section to behavioral tests. An architecture test states a convention for a directory, so these items do not apply to it.
|
||||
|
||||
- [ ] Each test covers observable behavior or an application contract, and passes after a change to the implementation that keeps the behavior.
|
||||
- [ ] Each tested declaration is exercised through behavior, and no test asserts the behavior of the framework. A test of what this project configures, such as a relation with a constraint, a cast, or a scope, belongs to this project.
|
||||
- [ ] Each test detects a distinct defect that no other test covers. A duplicate shrinks at the higher layer to the one case that proves the wiring.
|
||||
- [ ] Every changed decision and each applicable high-value failure mode has coverage.
|
||||
|
||||
## Names and Structure
|
||||
|
||||
- [ ] Each file has the name `{ClassName}Test.php` and the relative path of the class under test.
|
||||
- [ ] Each name states a result, the condition that causes it, and the status code for an API error.
|
||||
- [ ] Each file uses one declaration style consistently, and each `describe()` group holds separate behavior.
|
||||
|
||||
## The Coverage
|
||||
|
||||
- [ ] HTTP tests cover authentication, authorization, role, scope, and validation when applicable.
|
||||
- [ ] A request for a record of a different tenant gets a status code that does not confirm that the record exists.
|
||||
- [ ] The complete permission matrix belongs in policy tests, not controller tests.
|
||||
- [ ] Each validation rule has one test that asserts the user-visible message. When a unit test owns a matrix, reduce duplicate higher-level coverage to one case rather than deleting it.
|
||||
- [ ] Rendered user input and each dynamic part of a query have a security test.
|
||||
|
||||
## The Data and the Determinism
|
||||
|
||||
- [ ] Each test creates its mutable records directly or through a helper that it calls, and every created record arranges the behavior or supports an assertion.
|
||||
- [ ] Each `beforeEach()` holds configuration only.
|
||||
- [ ] Each factory state and each relationship gives the meaning of the data.
|
||||
- [ ] Each call to `make()` is in a test that does not need the database.
|
||||
- [ ] Time, randomness, sleep, and outbound HTTP are controlled.
|
||||
- [ ] Each test passes alone, and passes in the complete suite in any order.
|
||||
|
||||
## The Assertions
|
||||
|
||||
- [ ] Each expected value is a known value, and the test does not calculate the value with the logic of the implementation.
|
||||
- [ ] Each test of a write operation asserts the response, the state in the database, and the side effects.
|
||||
- [ ] Each fake has one assertion, and gives the class names unless the test asserts the complete result.
|
||||
- [ ] Each `expect()` chain stays on one subject.
|
||||
|
||||
## The Defects to Report
|
||||
|
||||
A review can find defects in the code rather than the tests. Report each defect below, and do not write a test that codifies it as correct behavior.
|
||||
|
||||
- [ ] A method with no body.
|
||||
- [ ] A policy that exists, but that no action calls.
|
||||
- [ ] A write action with no validation.
|
||||
- [ ] A status code or a response shape that is different from the shape of a similar endpoint.
|
||||
27
.agents/skills/testing-best-practices/rules/security.md
Normal file
27
.agents/skills/testing-best-practices/rules/security.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Security Tests
|
||||
|
||||
Test each security boundary where user input affects authorization, rendered output, or query construction. A defect at such a boundary can be difficult to detect because the feature may continue to work.
|
||||
|
||||
Write a test for each of these cases:
|
||||
|
||||
- **Cross-tenant access.** Request a record of a different tenant, team, or organization. Read `rules/endpoint-tests.md` for why the response should be `404` rather than `403`.
|
||||
- **Each unprivileged role.** Use a dataset over the roles that the endpoint must refuse.
|
||||
- **Escaping user-provided content.** Test escaping in HTML and mail. Include names and every free-text field a template renders. Assert that dangerous characters are escaped and the raw value is absent. Do not assert an exact entity for a quote, because Markdown and mail CSS inliners may decode it.
|
||||
- **Injection into dynamic query components.** Examples include sort columns, filter fields, and sort directions.
|
||||
- **An unexpected key** in a payload or configuration array. A merge that accepts every key can set an attribute the user must not control.
|
||||
|
||||
```php
|
||||
it('escapes dangerous content in the notification', function () {
|
||||
$organization = Organization::factory()->make([
|
||||
'name' => "O'Reilly <script>alert('xss')</script>",
|
||||
]);
|
||||
|
||||
$content = (new QuotaApproaching($organization, 80))->toMail()->render();
|
||||
|
||||
expect($content)
|
||||
->toContain('<script>')
|
||||
->not->toContain("<script>alert('xss')</script>");
|
||||
});
|
||||
```
|
||||
|
||||
Laravel provides defenses against mass assignment, unauthorized access, and unescaped output. Test that the application applies the appropriate defense to each attribute, route, and template.
|
||||
56
.agents/skills/testing-best-practices/rules/test-data.md
Normal file
56
.agents/skills/testing-best-practices/rules/test-data.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Factories and Test Data
|
||||
|
||||
## Each Test Makes Its Own Data
|
||||
|
||||
Create mutable records inside the test that uses them. This keeps setup visible and lets each test select its factory state.
|
||||
|
||||
Use `beforeEach()` only for configuration that applies to every test in the file. Do not create records in it.
|
||||
|
||||
## Record Construction
|
||||
|
||||
- Use `create()` if the test needs the record in the database.
|
||||
- Use `make()` only if the test does not need the database. Examples include rendering a notification and testing a value object's behavior.
|
||||
- Use a named factory state instead of a raw attribute. `User::factory()->unverified()->create()` gives the state meaning; `create(['email_verified_at' => null])` gives only its value.
|
||||
- Use `for()` or the relationship helper of the project to declare the owner of a record.
|
||||
- Use `recycle()` if several records must share one parent record.
|
||||
- Use `sequence()` if several records need different attributes.
|
||||
|
||||
```php
|
||||
$organization = Organization::factory()->onPlan(BillingPlan::PRO)->create();
|
||||
|
||||
$environment = Environment::factory()->recycle($organization)->create();
|
||||
|
||||
$organizations = Organization::factory()
|
||||
->count(3)
|
||||
->sequence(
|
||||
['created_at' => now()->setSeconds(30)],
|
||||
['created_at' => now()->setSeconds(1)],
|
||||
)
|
||||
->create();
|
||||
```
|
||||
|
||||
Create only the records required to arrange the behavior or support an assertion.
|
||||
|
||||
## The Datasets
|
||||
|
||||
Use a dataset when the setup, test body, and assertions remain the same across input values.
|
||||
|
||||
```php
|
||||
it('forbids roles other than admin', function (Role $role) {
|
||||
actingAs(User::factory()->hasOrganization($role)->create())
|
||||
->post('/settings')
|
||||
->assertForbidden();
|
||||
})->with(collect(Role::cases())->reject(fn (Role $role) => $role === Role::ADMIN));
|
||||
```
|
||||
|
||||
Use parameterized tests for:
|
||||
|
||||
- the cases of an enum
|
||||
- the roles and the plans
|
||||
- the boundary values
|
||||
- the input values that are not valid in the same way
|
||||
- the pairs of an input value and an output value
|
||||
|
||||
Write separate tests if the cases need a different setup, a different behavior, or different assertions. One test function with a branch in the body is two tests in one function.
|
||||
|
||||
Give each dataset case a name that states the difference. A failure then identifies the case without requiring you to count positions.
|
||||
35
.editorconfig
Normal file
35
.editorconfig
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{php,json}]
|
||||
indent_size = 4
|
||||
|
||||
[*.blade.php]
|
||||
indent_size = 2
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[{compose,docker-compose}.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.scss]
|
||||
indent_size = 2
|
||||
quote_type = single
|
||||
|
||||
[*.css]
|
||||
indent_size = 2
|
||||
quote_type = single
|
||||
|
||||
[*.vue]
|
||||
indent_size = 2
|
||||
89
.env.example
Normal file
89
.env.example
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
# PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_URL=
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
# DB_SOCKET=
|
||||
# DB_CHARSET=utf8mb4
|
||||
# DB_COLLATION=utf8mb4_unicode_ci
|
||||
# MYSQL_ATTR_SSL_CA=
|
||||
|
||||
ARCADIA_DB_CONNECTION=mysql
|
||||
# ARCADIA_DB_URL=
|
||||
ARCADIA_DB_HOST=127.0.0.1
|
||||
ARCADIA_DB_PORT=3306
|
||||
ARCADIA_DB=arcadia
|
||||
ARCADIA_DB_USERNAME=root
|
||||
ARCADIA_DB_PASSWORD=
|
||||
# ARCADIA_DB_SOCKET=
|
||||
# ARCADIA_DB_CHARSET=utf8mb4
|
||||
# ARCADIA_DB_COLLATION=utf8mb4_unicode_ci
|
||||
# ARCADIA_DB_SSL_CA=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
ARCADIA_CACHE_STORE=redis
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
SECURITY_ALERT_EMAIL="security@example.com"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
THROTTLE_WEB=120
|
||||
THROTTLE_API_V1=60
|
||||
|
||||
ARCADIA_AUTH_DEV_BYPASS=false
|
||||
41
.env.testing
Normal file
41
.env.testing
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
APP_NAME="Arcadia Api"
|
||||
APP_ENV=testing
|
||||
APP_KEY=base64:2ik2NRSDu8/Lxsxzdnnom3xWEglXe+cGd/7beOt/eXY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
|
||||
BCRYPT_ROUNDS=4
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
DB_DATABASE=:memory:
|
||||
|
||||
ARCADIA_DB_CONNECTION=sqlite
|
||||
|
||||
SESSION_DRIVER=array
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
|
||||
BROADCAST_CONNECTION=null
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
CACHE_STORE=array
|
||||
MAIL_MAILER=array
|
||||
|
||||
ARCADIA_CACHE_STORE=array
|
||||
|
||||
SPEC_PATH=documentation/OpenApi
|
||||
|
||||
PULSE_ENABLED=false
|
||||
TELESCOPE_ENABLED=false
|
||||
NIGHTWATCH_ENABLED=false
|
||||
ARCADIA_AUTH_DEV_BYPASS=true
|
||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
104
.github/skills/infer-conventions/SKILL.md
vendored
Normal file
104
.github/skills/infer-conventions/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
name: infer-conventions
|
||||
description: "Use this skill to analyze how a Laravel application is actually written and record its conventions as shared rules. Trigger when the user wants to detect, infer, document, or standardize project conventions or coding style, set up or grow `.ai/rules`, resolve mixed or conflicting patterns (e.g. \"are we using Form Requests or inline validation?\"), or onboard agents and teammates to \"how we do things here\". Covers: a systematic sweep of ~49 Laravel convention dimensions (validation, models, architecture, testing, frontend, database, console), open-ended house-pattern discovery, conflict reporting, and recording rules scoped to the right paths via the Boost `record-rule` MCP tool. Do not use for one-off code review, enforcing formatting a linter already handles, or editing `.ai/rules` files by hand."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Infer Conventions
|
||||
|
||||
Learn how this application writes Laravel, then record what you learn as durable, path-scoped rules other agents will read. You are documenting reality, not improving it.
|
||||
|
||||
## Ground Rules (read before you start)
|
||||
|
||||
- Consistency first. The codebase's majority style is the convention. Never judge it, never propose a "better" pattern, never record what the code should do. If the app validates inline everywhere, that is the rule, even if Form Requests would be nicer.
|
||||
- Skip what an active tool produces, keep what a tool would fight. Inspect the project's Pint and Rector configuration first; a Rector transformation is tooling-owned only when its package and relevant rule or set are installed and enabled. Active tools may rewrite code toward one canonical form: `$casts` to `casts()`, `$fillable` to attributes, magic accessors to the `Attribute` class, pipe-string rules to arrays, `$signature` to `#[Signature]`, named migrations to anonymous, and many more. When the app already sits at an active tool's target form, the tool owns it, so record nothing. But when the app deliberately holds a form an active tool would refactor away, such as legacy `getXxxAttribute()` accessors the `Attribute` class would replace, no tool can reproduce that choice and an agent defaults the other way. That against-the-grain hold is exactly what to record.
|
||||
- Record decisions, not defaults. A consistent pattern earns a rule only when it reflects a choice: the app took one valid option where the framework or common practice offered others, or the pattern would surprise a competent agent. Framework defaults steer nothing, so skip them: anonymous migrations, `$signature` commands, `ShouldQueue` jobs, `casts()` on Laravel 11+, named routes, Rule objects in `app/Rules`, and `Mail::fake()` or `Bus::fake()` to isolate framework services. A real fork is not enough on its own. Weigh the side the app took, and record only the side an agent would not reach for by itself: inline closures everywhere, legacy accessors, a bespoke query layer. Watch for the false fork too. "No Mockery" next to facade fakes is not a choice against Mockery, because they double different things. The test for every candidate: without this rule, would the next agent plausibly write it differently? Only "yes" earns a rule.
|
||||
- Architecture choices are the gold. Record presence and deliberate absence. The structural pattern the app commits to is the highest-signal convention and the one no tool can decide: Action classes and how they are invoked (`handle` / `execute` / `__invoke`), service objects, dedicated query objects exposing `builder()`, DTOs (spatie/laravel-data vs readonly classes), Form Request validation vs inline, an events and listeners spine vs direct calls, and domain or module folders. Also record a consistent non-pattern, such as "query Eloquent directly in controllers, no repository layer", so the next agent matches the app's altitude instead of over-engineering.
|
||||
- Never duplicate `.ai/rules`. Read `.ai/rules/index.md` and the area files before the sweep. A dimension already covered there is marked done and skipped.
|
||||
- Evidence or silence. A convention needs at least 3 consistent examples and no meaningful rival to become a candidate. Every Step 1 verdict applies this bar.
|
||||
- The recorded rule states the convention, nothing else. One or two imperative lines: this project does X, so do X here. Keep detection evidence out. No counts, ratios, current usage, file lists, or example paths, because that is proof for the confirm step, not part of the rule. One short syntax fragment at most, and point to `search-docs` for API details.
|
||||
|
||||
## Process
|
||||
|
||||
Each step ends on a checkable completion criterion. Do not advance until it holds.
|
||||
|
||||
Fan out when you can. The sweep is embarrassingly parallel. If your environment can spawn subagents (a Task, dispatch, or equivalent tool), do Step 0 yourself, then hand each checklist group (A to J) and the architecture map to its own subagent. Each subagent runs the greps, reads a few representative files, and returns structured verdicts (dimension, verdict, evidence, proposed glob / title / note). You aggregate, dedupe, then run Steps 3 to 5. It is far faster on a real app. No subagents available? Run the steps in sequence, with the same bar and the same output.
|
||||
|
||||
### Step 0: Orient
|
||||
|
||||
Read `composer.json` (installed packages tell you which checklist groups apply), the `pint.json` / PHPStan / Rector config, `.ai/rules/index.md` if present, and most important, map the `app/` tree. List every directory under `app/` (and any `Modules/`, `src/`, `packages/`, or domain root). Every folder beyond Laravel's default skeleton (`Http`, `Models`, `Providers`, `Console`, `Exceptions`) is a structural pattern the app committed to and a high-value rule waiting to be written: `Actions`, `Services`, `Data` or DTOs, `Queries`, `Repositories`, `ViewModels`, `Pipelines`, `Support`, `Enums`, `Contracts`, `Observers`, or `Domain` and module roots. Note each one. You will confirm how it is used in Step 2.
|
||||
|
||||
This app has no Livewire/Inertia/Flux packages installed. Treat the frontend group as likely API-only: confirm from `resources/views` before spending time there, and skip the Livewire/Inertia/Flux dimensions.
|
||||
|
||||
Done when: you have the applicable checklist groups, the dimensions already recorded in `.ai/rules`, and a list of every non-default `app/` directory mapped to the pattern it represents.
|
||||
|
||||
### Step 1: Predefined sweep
|
||||
|
||||
Open `references/checklist.md` and work every applicable dimension using its search hints. Give each exactly one verdict:
|
||||
|
||||
- Pattern. Clears the bar, rival under ~20% of sites, and reflects a real choice (passes the decisions-not-defaults test). A recording candidate. Cite 2 to 3 example files.
|
||||
- Conflict. Both styles present in meaningful numbers. Report the split with counts and example files. Never record a preferred winner while the code remains mixed, even in yolo, because that would describe an aspiration rather than reality. Record only if the user identifies a stable path or context boundary that explains both styles; otherwise defer until the code is reconciled.
|
||||
- Default. Consistent, but a framework or common-practice default the agent already writes unprompted. Skip it as a no-op, not a convention.
|
||||
- No signal. Under the bar: feature unused, or too few examples. Skip silently (one summary line at most).
|
||||
- Tooling-owned or Already-recorded. Skip per the ground rules.
|
||||
|
||||
Done when: every applicable dimension carries exactly one of those verdicts.
|
||||
|
||||
### Step 2: Open-ended pass
|
||||
|
||||
First, close out the architecture map from Step 0. For every non-default `app/` directory you listed, confirm how the pattern is used and apply the same evidence and decisions-not-defaults tests as Step 1. Generator-standard or sparsely used directories such as `Rules`, `Observers`, `Mail`, and `Notifications` are signals to inspect, not automatic conventions. Make genuine structural patterns candidates: Action classes invoked via `handle` / `execute` / `__invoke`, Services constructor-injected, `Queries` objects exposing `builder(): Builder`, DTOs as readonly classes or spatie/laravel-data, module or domain folders as the unit of organization. Scope each qualifying pattern to its own directory glob. Also record a consistent deliberate absence, such as "no repository layer, controllers query Eloquent directly", so the next agent matches the app's altitude.
|
||||
|
||||
Then find what else makes this codebase itself: base or abstract classes most code extends, traits used everywhere, tenancy or authorization scoping woven through queries, naming schemes, and custom helpers. Same evidence bar, cite files. Record every genuine structural pattern, and cap the other house findings at ~5 so the pass stays high-signal.
|
||||
|
||||
Done when: every non-default `app/` directory from Step 0 has a verdict, and the pass has produced its cited house findings (or concluded there are none).
|
||||
|
||||
### Step 3: Confirm
|
||||
|
||||
Present every candidate in one batch. Per item: dimension, verdict, evidence (counts and files), and the exact proposed `glob` or `globs` / `title` / `note`. Conflicts are presented as questions about an existing context boundary or deferred cleanup, not as a choice of future style.
|
||||
|
||||
Default mode is confirm: record only what the user approves. Switch to yolo only when the invocation said so ("yolo", "don't ask", "just record them"), then record all pattern candidates without asking. Conflicts still go to the user in yolo.
|
||||
|
||||
Done when: every candidate is approved, rejected, or (conflicts) decided.
|
||||
|
||||
### Step 4: Record
|
||||
|
||||
Make one `record-rule` call for each glob an approved convention applies to. Choose the most specific globs that cover the cited evidence from the mapping table below; if a convention spans models and migrations, record it under both domains so agents discover it from either path. The `note` is the bare convention: strip every trace of detection (see the ground rule). If `record-rule` is unavailable (rules disabled), report the full rule text so the user can enable `BOOST_RULES_ENABLED` or add it by hand.
|
||||
|
||||
Record this:
|
||||
|
||||
> Accessors and mutators: use the legacy magic-method style (`getXxxAttribute()` / `setXxxAttribute()`), not the `Attribute` class. Match it in models.
|
||||
|
||||
Not this:
|
||||
|
||||
> Accessors/mutators use the legacy magic-method style; the `Attribute`-class style is not used anywhere (13 legacy, 0 Attribute-class), e.g. `app/Models/Post.php`. Match the legacy style in existing models.
|
||||
|
||||
Done when: every approved item has a successful tool response, and any failure is reported with its rule text.
|
||||
|
||||
### Step 5: Summarize
|
||||
|
||||
List recorded rules (file and title), conflicts the user deferred, notable no-signals, and remind the user to commit `.ai/rules` so their team and agents share the conventions.
|
||||
|
||||
## Glob mapping
|
||||
|
||||
Attach each rule to the most specific path that covers its evidence. Never a lazy `app/**` when a subtree fits. Match the glob to where the code actually lives, which is not the same in a default skeleton and in a modular or DDD layout. Use the Step 0 `app/` map to pick the real path.
|
||||
|
||||
Examples:
|
||||
|
||||
- Models: `app/Models/**` in a default app, or `app/Modules/Blog/Models/**` / `src/Domain/Blog/**` in a modular one.
|
||||
- Controllers, routing, validation, responses: `app/Http/**`, or `app/Modules/*/Http/**` when each module owns its HTTP layer.
|
||||
- Actions, Services, DTOs: `app/Actions/**`, `app/Services/**`, `app/Data/**`, or the module path the app actually uses.
|
||||
- Tests: `tests/**`.
|
||||
- Migrations and database: `database/migrations/**`.
|
||||
- Truly app-wide (rare, e.g. auth retrieval): `app/**`.
|
||||
|
||||
`record-rule` takes one glob. When a convention genuinely spans two domains (e.g. UUID keys touch models and migrations), call it once per domain with the same title and note; mentioning another path in the note does not make the rule discoverable there.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- Rules disabled or `record-rule` missing: detection is read-only, so Steps 0 to 3 still run, and recording falls back to the manual path in Step 4.
|
||||
- Tiny or fresh app: most dimensions land on no-signal. Say so honestly ("not enough code to infer conventions yet") and record nothing.
|
||||
- Huge app: each dimension is a bounded grep plus a handful of file reads. Sample representative files, do not read everything.
|
||||
- Re-runs: reading `.ai/rules` in Step 0 makes re-runs incremental, so only new or undecided dimensions surface.
|
||||
- Non-standard layout (modules, DDD): the open-ended pass catches the layout itself as convention #1. Adapt the globs in the mapping table to the observed paths.
|
||||
137
.github/skills/infer-conventions/references/checklist.md
vendored
Normal file
137
.github/skills/infer-conventions/references/checklist.md
vendored
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Detection Checklist
|
||||
|
||||
Every dimension here is a genuine fork: Laravel offers two or more valid approaches, the app's choice changes what the next agent writes, and no active project tool can pick for you. Left out on purpose: pure formatting (Pint owns it), any form an installed and enabled Rector rule rewrites to one canonical shape (`$casts` to `casts()`, `$fillable` to attributes, pipe-string rules to arrays, named to anonymous migrations, `$signature` to `#[Signature]`), and framework defaults any agent writes unprompted (`ShouldQueue` jobs, relation return types, `HasFactory`).
|
||||
|
||||
Each item gives the fork, then a hint (a grep or dir to spot which side the app takes). Hints are only a start. Read the matched files, never record on a raw count. Apply the ground rules to every verdict: a consistent choice that is a default or a tool's target form is not a pattern. Rows tagged (architecture) are the highest-signal, so record presence and deliberate absence.
|
||||
|
||||
---
|
||||
|
||||
## A. Validation & HTTP input
|
||||
|
||||
1. Validation entry point: inline `$request->validate()` vs Form Request classes vs `Validator::make()`.
|
||||
- Hint: `ls app/Http/Requests`; grep `->validate(` / `Validator::make(` in `app/Http/Controllers`.
|
||||
2. Custom rule location: invokable rule objects in `app/Rules` vs inline closures vs `Validator::extend()` in a provider. Rule objects are the default `make:rule` path, so record only if the app leans on closures or `Validator::extend` instead. "No rule objects" alone is just no-signal.
|
||||
- Hint: `ls app/Rules`; grep `Validator::extend` in `app/Providers`.
|
||||
3. Typed input retrieval: typed getters (`$request->string()`, `->integer()`, `->enum()`, `->date()`) vs raw `$request->input()` / dynamic properties.
|
||||
- Hint: grep `->string(` / `->integer(` / `->enum(` vs `->input(` in `app/Http`.
|
||||
4. Custom messages/attributes: `lang/*/validation.php` vs Form Request `messages()` / `attributes()` methods.
|
||||
- Hint: `ls lang`; grep `function messages`, `function attributes` in `app/Http/Requests`.
|
||||
|
||||
## B. Controllers & routing
|
||||
|
||||
5. Controller shape: invokable single-action (`__invoke`) vs resource controllers vs plain multi-method.
|
||||
- Hint: grep `__invoke` in controllers; `Route::resource` / `apiResource` vs verb routes.
|
||||
6. Business-logic location (architecture): fat controllers vs delegated to Actions / Services / Jobs.
|
||||
- Hint: read a few controller methods; `ls app/Actions app/Services`.
|
||||
7. Route handler style: closures in `routes/*.php` vs controller classes.
|
||||
- Hint: count `function ()` vs `::class` in `routes/web.php`, `routes/api.php`.
|
||||
8. Middleware assignment: route/group `->middleware()` vs controller `HasMiddleware::middleware()` vs `#[Middleware]` attribute.
|
||||
- Hint: grep `implements HasMiddleware`, `#[Middleware(` in controllers vs `->middleware(` in routes.
|
||||
9. Route model binding: implicit (type-hinted models) vs explicit `Route::bind` vs manual `findOrFail`.
|
||||
- Hint: typed model params in signatures vs `findOrFail(` in controllers; grep `Route::bind`.
|
||||
10. Rate limiting: named `RateLimiter::for()` + `throttle:name` vs inline `throttle:60,1`.
|
||||
- Hint: grep `RateLimiter::for` in providers vs `throttle:` in route files.
|
||||
|
||||
## C. Authorization
|
||||
|
||||
11. Authorization home: Gates (`Gate::define`) vs Policy classes in `app/Policies`.
|
||||
- Hint: `ls app/Policies`; grep `Gate::define` in `app/Providers`.
|
||||
12. Authorization call site: `$this->authorize()` / `Gate::authorize()` vs `$user->can()` vs `can` middleware vs `#[Authorize]` vs `@can` in Blade.
|
||||
- Hint: grep `authorize(`, `->can(`, `middleware('can:`, `#[Authorize(`, `@can(`.
|
||||
|
||||
## D. Eloquent & models
|
||||
|
||||
13. Mass assignment: `$fillable` allow-list vs `$guarded` block-list.
|
||||
- Hint: grep `protected $fillable` / `protected $guarded` in `app/Models`.
|
||||
14. Accessors/mutators: modern `Attribute` class vs legacy `getXxxAttribute()` / `setXxxAttribute()`. Record a legacy hold, it goes against the tool's grain.
|
||||
- Hint: grep `: Attribute` / `Attribute::make` vs `function get[A-Z].*Attribute` in `app/Models`.
|
||||
15. Primary keys: auto-increment vs `HasUuids` vs `HasUlids`.
|
||||
- Hint: grep `HasUuids` / `HasUlids` in `app/Models`; migration `id()` vs `uuid('id')`.
|
||||
16. Custom casts: dedicated `CastsAttributes` classes (`app/Casts`) vs inline `Attribute` vs built-in cast strings.
|
||||
- Hint: `ls app/Casts`; grep `Cast::class`, `AsStringable::class` in models.
|
||||
17. Data/query layer (architecture): Eloquent directly in controllers vs repositories vs dedicated query objects (e.g. classes exposing `builder(): Builder`).
|
||||
- Hint: `ls app/Repositories app/Queries`; see where non-trivial queries are built.
|
||||
18. Query scopes: local `scope`/`#[Scope]` methods vs dedicated builder classes.
|
||||
- Hint: grep `function scope` / `#[Scope]` in models; `ls app/*/Builders`.
|
||||
19. Model events: observers (`app/Observers`, `#[ObservedBy]`) vs `booted()` closures vs event classes.
|
||||
- Hint: `ls app/Observers`; grep `booted`, `::observe`, `#[ObservedBy]`.
|
||||
20. Eager-load posture: explicit per-query `->with()` vs model-level `$with` defaults. Treat `preventLazyLoading()` separately as a development guard because it can complement either posture.
|
||||
- Hint: grep `protected $with`, `->with(`, and separately `preventLazyLoading` in `app/`.
|
||||
|
||||
## E. Architecture & organization
|
||||
|
||||
21. Action/Service structure (architecture): Action classes (invoked via `handle` / `execute` / `__invoke`) vs service objects vs neither. Cross-check the Step 0 `app/` map: any `Actions`/`Services`/`Pipelines`/`Jobs`-as-actions folder is this pattern, so record how it is invoked.
|
||||
- Hint: `ls app/` (the whole tree, not just `Actions`/`Services`); grep the invocation method in the folder you find.
|
||||
22. DTOs (architecture): spatie/laravel-data vs plain readonly classes vs arrays everywhere.
|
||||
- Hint: `ls app/Data`; grep `extends Data`, `readonly class` in `app/`.
|
||||
23. Dependency acquisition: constructor/method injection vs `app()` / `resolve()` / `App::make()` service location.
|
||||
- Hint: grep `app(` / `resolve(` / `::make(` in `app/` vs promoted constructor deps.
|
||||
24. Decoupling: events + listeners vs direct service calls.
|
||||
- Hint: `ls app/Events app/Listeners`; grep `event(`, `::dispatch(`.
|
||||
25. Helper vs facade idiom: global helpers (`config()`, `auth()`, `response()`) vs facades (`Config::`, `Auth::`, `Response::`).
|
||||
- Hint: ratio of `config(` vs `Config::` (etc.) across `app/`.
|
||||
26. Namespace layout (architecture): default `app/` skeleton vs domain/module folders (`app/Domain/**`, modules).
|
||||
- Hint: `ls app/`, look for `Domain/`, `Modules/`, bounded-context folders.
|
||||
27. Enums: backed vs pure; case naming; where they live.
|
||||
- Hint: `ls app/Enums`; grep `enum .*: string`, `enum .*: int`.
|
||||
|
||||
## F. Frontend & views
|
||||
|
||||
No Livewire/Inertia/Flux package is installed. This app may be API-only. Confirm from `resources/views` before sweeping, and treat the Livewire/Flux dimensions as not applicable.
|
||||
|
||||
28. Frontend stack: Blade+Livewire vs Inertia (Vue/React/Svelte) vs Blade-only / API + separate SPA.
|
||||
- Hint: `composer.json` + `package.json`; `ls resources/js/pages`, `resources/views`.
|
||||
29. Blade composition: class `<x-*>` components vs anonymous components (`@props`) vs `@include` partials.
|
||||
- Hint: `ls app/View/Components`; grep `<x-`, `@include` in `resources/views`.
|
||||
32. Localization: short keys (`lang/*/*.php` + `__('messages.welcome')`) vs JSON string keys (`lang/*.json` + `__('Full sentence')`).
|
||||
- Hint: `ls lang`; grep dotted `__('` vs sentence keys.
|
||||
|
||||
## G. Database & migrations
|
||||
|
||||
33. Foreign keys: `foreignId()->constrained()` vs `foreignIdFor(Model::class)` vs manual `foreign()->references()->on()`.
|
||||
- Hint: grep `foreignId(`, `foreignIdFor(`, `->foreign(` in `database/migrations`.
|
||||
34. `down()` methods: real reverse logic vs omitted / one-way migrations.
|
||||
- Hint: grep `function down` vs the migration count.
|
||||
35. Enum storage: DB `enum()` column vs `string()` + PHP-enum cast on the model.
|
||||
- Hint: grep `->enum(` in migrations vs string columns cast to enums.
|
||||
36. Transactions: `DB::transaction(fn ...)` closure vs manual `beginTransaction` / `commit` / `rollBack`.
|
||||
- Hint: grep `DB::transaction`, `beginTransaction` in `app/`.
|
||||
37. Idempotent writes: `upsert` / `updateOrCreate` / `firstOrCreate` vs find-then-save.
|
||||
- Hint: grep `upsert(`, `updateOrCreate(`, `firstOrCreate(` in `app/`.
|
||||
|
||||
## H. Testing
|
||||
|
||||
38. Framework: Pest (`it()` / `test()` / `expect()`) vs PHPUnit classes.
|
||||
- Hint: `ls tests/Pest.php`; grep `it(` / `test(` vs `extends TestCase`.
|
||||
39. DB reset: `RefreshDatabase` vs `DatabaseTruncation` vs `DatabaseMigrations`.
|
||||
- Hint: grep those trait names in `tests/`.
|
||||
40. Fixtures: compare how equivalent test-owned records are created, such as factories vs manual inserts. Track seeders separately for shared reference data because `$this->seed()` commonly and legitimately coexists with factories.
|
||||
- Hint: grep `::factory(` and direct inserts in `tests/`; separately inspect `$this->seed(` calls and what those seeders provide.
|
||||
41. Collaborator isolation: how the app doubles its own classes, Mockery `mock()` / `spy()` vs real integration. Ignore facade fakes like `Mail::fake()` here, they isolate framework services by default and are not a fork against Mockery.
|
||||
- Hint: grep `->mock(`, `->spy(`, `Mockery::` in `tests/`.
|
||||
42. Endpoint assertions: array `assertJson([...])` / `assertJsonFragment` vs fluent `AssertableJson`.
|
||||
- Hint: grep `AssertableJson`, `assertJsonFragment` in `tests/`.
|
||||
|
||||
## I. Responses & API resources
|
||||
|
||||
43. Response shape: API Resource classes vs `response()->json()` vs returning models/arrays directly.
|
||||
- Hint: `ls app/Http/Resources`; grep `JsonResource`, `->json(` in controllers.
|
||||
44. Resource relationship inclusion: `whenLoaded()` guards vs unconditional relationship access. Do not count ordinary scalar attributes as rivals to conditional relationships, and evaluate general `when()` fields separately.
|
||||
- Hint: compare relationship fields using `whenLoaded(` with unconditional relationship property access in `app/Http/Resources`.
|
||||
45. Pagination contracts: within comparable endpoint categories, length-aware `paginate()` vs `simplePaginate()` vs `cursorPaginate()`. These have different totals, navigation, ordering, and performance contracts, so record only a stable path-scoped API policy, never a project-wide majority.
|
||||
- Hint: grep those in `app/`, then group matches by endpoint type and client contract before comparing them.
|
||||
46. Web redirects/URLs: `route('name')` vs `url('/path')` vs `action([...])`.
|
||||
- Hint: grep `route('`, `url('/`, `action([` in `app/Http` and views.
|
||||
|
||||
## J. Strings, collections & dates
|
||||
|
||||
47. Iteration idiom: `collect()->map()->filter()` pipelines vs `array_map` / `foreach`.
|
||||
- Hint: grep `collect(`, `->map(` vs `array_map`, `foreach` density in `app/`.
|
||||
48. String API: fluent `Str::of()->...` (Stringable) vs static `Str::` vs native (`trim`, `strtoupper`).
|
||||
- Hint: grep `Str::of(` vs `Str::` vs native string funcs.
|
||||
49. Dates: compare equivalent construction call styles (`now()` / `today()` helpers vs `Carbon::`) separately from the application's mutable/immutable date policy. `Date::use(CarbonImmutable::class)` can make helpers return immutable dates, so those signals are complementary rather than conflicting.
|
||||
- Hint: grep `now(` and `Carbon::` for call style; separately inspect `CarbonImmutable` and `Date::use` for mutability policy.
|
||||
|
||||
---
|
||||
|
||||
Genuine forks only. Every row survived the "no tool can decide this, and it isn't the default" filter. Give each applicable dimension exactly one verdict: pattern, conflict, default, no-signal, tooling-owned, or already-recorded. The rows tagged (architecture) are where the highest-value rules come from.
|
||||
51
.github/skills/laravel-audit-architecture/SKILL.md
vendored
Normal file
51
.github/skills/laravel-audit-architecture/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
name: laravel-audit-architecture
|
||||
description: >
|
||||
Audit a Laravel application's architecture: boundaries, responsibility,
|
||||
coupling, duplication, and maintainability. Use when auditing architecture.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Architecture Audit
|
||||
|
||||
Audit the architecture of the Laravel application. **Avoid cargo-cult recommendations** such as "create a repository for every model" or "move everything into services."
|
||||
|
||||
List applicable rules first:
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules --domain=architecture --applicable
|
||||
```
|
||||
|
||||
## What to look for
|
||||
|
||||
- Clear violations of application boundaries: domain logic leaking into controllers, routes, or views; persistence concerns in presentation layers.
|
||||
- Duplicated business logic: the same rule implemented in several places with divergence risk.
|
||||
- Excessive controller responsibility: controllers doing far more than orchestrating HTTP.
|
||||
- Business logic in inappropriate layers: rules embedded in views, middleware, or migrations.
|
||||
- Tightly coupled components: classes that are hard to test or change because of hidden dependencies.
|
||||
- Repeated patterns that have become maintenance problems: copy-pasted blocks that should be shared.
|
||||
- Inconsistent architectural conventions: half the app using one pattern and half another without reason.
|
||||
- Unnecessary abstractions: interfaces, repositories, or services that add indirection without value.
|
||||
- Dangerous service/repository patterns: abstraction layers that create complexity rather than value.
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
Every architecture finding needs at least:
|
||||
|
||||
- The specific class(es) and file/line ranges.
|
||||
- Why the pattern is a problem in this codebase (not just "best practice").
|
||||
- A concrete improvement that fits the existing style.
|
||||
|
||||
## False positives
|
||||
|
||||
- Small controllers that orchestrate a couple of calls are fine.
|
||||
- A service layer is not automatically bad; only flag it when it adds complexity without value.
|
||||
- Do not impose a single architecture style the codebase does not otherwise use.
|
||||
|
||||
## Severity guidance
|
||||
|
||||
- `high`: structure actively blocks maintenance or causes bugs (e.g. duplicated critical logic diverging).
|
||||
- `medium`: clear maintainability problem with real cost.
|
||||
- `low`: stylistic or mild structure concern.
|
||||
- `info`: observations for future work.
|
||||
50
.github/skills/laravel-audit-conventions/SKILL.md
vendored
Normal file
50
.github/skills/laravel-audit-conventions/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
name: laravel-audit-conventions
|
||||
description: >
|
||||
Audit a Laravel application for framework conventions: anti-patterns,
|
||||
version-appropriate APIs, and misuse of framework features. Use when
|
||||
auditing Laravel conventions.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Conventions Audit
|
||||
|
||||
Audit the application for Laravel framework conventions. **The rules must be version-aware whenever the installed version is known.**
|
||||
|
||||
List applicable rules first:
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules --domain=conventions --applicable
|
||||
```
|
||||
|
||||
## What to look for
|
||||
|
||||
- Framework anti-patterns: patterns that fight the framework's intended flow.
|
||||
- Unnecessary custom code where standard Laravel mechanisms are clearly better: custom auth loops instead of guards, manual pagination instead of `paginate`, custom file handling instead of storage, etc.
|
||||
- Version-inappropriate APIs when the version is known: deprecated or removed methods, old-style helpers, `facades` used in ways removed in the installed version.
|
||||
- Incorrect framework assumptions: `config()` keys that don't exist, middleware that isn't applied, routes that don't match registered patterns.
|
||||
- Misuse of framework lifecycle/features: service providers doing work in `register()`, observers/mutators causing side effects, cache keys without invalidation, queueable misuse, scheduling misconfig.
|
||||
- Configuration, validation, routing, jobs, events, policies, resources, commands, notifications, mail, queues, scheduling, and caching used in problematic ways.
|
||||
- Abandoned runtime dependencies (`AUD-DEP-002`) or license conflicts for a commercial product (`AUD-DEP-003`) — only with `composer show` / license evidence, not because a package is old.
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
Every conventions finding needs at least:
|
||||
|
||||
- The installed Laravel version (from `project_info`).
|
||||
- The code in question (file + line/range).
|
||||
- The idiomatic Laravel alternative and why it is better.
|
||||
|
||||
## False positives
|
||||
|
||||
- Custom code may exist for good reasons the framework mechanism cannot serve — verify before recommending replacement.
|
||||
- Only recommend replacement when the standard mechanism is clearly better.
|
||||
- Do not flag style preferences as findings.
|
||||
|
||||
## Severity guidance
|
||||
|
||||
- `high`: usage that is broken or will break on upgrade.
|
||||
- `medium`: clear anti-pattern with maintenance cost.
|
||||
- `low`: minor convention drift.
|
||||
- `info`: observations.
|
||||
51
.github/skills/laravel-audit-database/SKILL.md
vendored
Normal file
51
.github/skills/laravel-audit-database/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
name: laravel-audit-database
|
||||
description: >
|
||||
Audit a Laravel application's database layer: schema, indexes, relationships,
|
||||
migrations, and query efficiency. Use when auditing the database layer.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Database Audit
|
||||
|
||||
Audit the database layer of the Laravel application. **Where possible, use structured schema information** (via the `database_schema` tool) rather than relying only on migrations.
|
||||
|
||||
List applicable rules first:
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules --domain=database --applicable
|
||||
```
|
||||
|
||||
## What to look for
|
||||
|
||||
- Missing or suspicious indexes when evidence supports them: frequently filtered/joined columns without indexes.
|
||||
- Schema inconsistencies: columns that contradict domain invariants or usage.
|
||||
- Inefficient relationships: relationship definitions that force expensive queries.
|
||||
- Suspicious migrations: destructive operations, deadlocks, or migrations that drop data without a path back.
|
||||
- Duplicate data modeling: the same fact stored in multiple tables/columns.
|
||||
- Bad foreign key choices: missing FKs, wrong ON DELETE behavior, or FKs to the wrong tables.
|
||||
- Nullable/non-nullable mismatches where they create risk.
|
||||
- Query inefficiencies: full-table scans on large tables, missing index coverage for common filters.
|
||||
- Relationship definitions inconsistent with schema or usage: `foreignId` that does not match the relationship, missing pivot table indexes, etc.
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
Every database finding needs at least:
|
||||
|
||||
- The specific table/column (or migration file + line).
|
||||
- The query pattern or relationship that is affected.
|
||||
- Schema facts from the `database_schema` tool or migrations.
|
||||
|
||||
## False positives
|
||||
|
||||
- Small tables do not need indexes on every column.
|
||||
- An index that already exists (or is covered by a composite index) is not missing.
|
||||
- Do not flag nullable columns when the domain allows null.
|
||||
|
||||
## Severity guidance
|
||||
|
||||
- `high`: schema/relationship mismatch causing real bugs or data integrity risk.
|
||||
- `medium`: clear inefficiency or inconsistency with real impact.
|
||||
- `low`: minor schema hygiene.
|
||||
- `info`: observations.
|
||||
143
.github/skills/laravel-audit-dsa/SKILL.md
vendored
Normal file
143
.github/skills/laravel-audit-dsa/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
---
|
||||
name: laravel-audit-dsa
|
||||
description: >
|
||||
Run a read-only, orchestrated audit of a Laravel codebase for data
|
||||
structures, state representation, algorithms, and ownership. Use when asked
|
||||
for a DSA audit, subsystem inventory, or Aaron-style bounded agent audit.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel DSA / organizing-model audit
|
||||
|
||||
Audit the Laravel application for **materially useful simplifications** in data structures, state representation, control flow, algorithms, and ownership.
|
||||
|
||||
This is an **audit-only** exercise. Do not edit files, run mutating commands, implement recommendations, commit, or push. Read-only inspection and Laravel Auditor context tools are allowed.
|
||||
|
||||
You are the **coordinator**. Continue until every identifiable subsystem has been reviewed and the final audit is validated.
|
||||
|
||||
Credit: this workflow follows Aaron Francis's bounded, read-only subsystem audit method.
|
||||
|
||||
## Tools to use first
|
||||
|
||||
```bash
|
||||
php artisan auditor:context subsystems
|
||||
php artisan auditor:context project_info
|
||||
php artisan auditor:rules --applicable
|
||||
```
|
||||
|
||||
Also use `routes`, `models`, `database_schema`, `policies_authorization`, `jobs_events_schedules`, and `tests` as needed. Prefer these over raw guesses.
|
||||
|
||||
Map each finding to a stable rule when one fits (`AUD-DSA-001`–`AUD-DSA-004`, or a core domain rule). Put `subsystem` and `priority` (`p0`–`p3`) in finding `metadata`.
|
||||
|
||||
## 1. Establish the coverage contract
|
||||
|
||||
Inspect the repository and inventory every identifiable subsystem. Start from `auditor:context subsystems`, then add any leftover `app/` areas it did not list.
|
||||
|
||||
Give each subsystem:
|
||||
|
||||
- a stable ID and descriptive name
|
||||
- an exact ownership boundary
|
||||
- its key implementation files
|
||||
- relevant public interfaces, major call sites, and tests
|
||||
- a status: `queued`, `in review`, `recommend`, or `skip`
|
||||
|
||||
Include HTTP, models, auth, database, jobs/queues, events, console, mail/notifications, tests, config, frontend/Livewire/Inertia/Filament when present, and generated-contract ownership where relevant.
|
||||
|
||||
Create one canonical report containing:
|
||||
|
||||
- the subsystem inventory
|
||||
- confirmed opportunities
|
||||
- explicit skip decisions
|
||||
- cross-cutting patterns
|
||||
- duplicates and superseded findings
|
||||
- final priorities and dependencies
|
||||
- an audit log
|
||||
|
||||
Treat this inventory as the coverage contract. Do not assume a catch-all row proves coverage.
|
||||
|
||||
## 2. Run bounded subsystem reviews
|
||||
|
||||
Use fresh, read-only agents where available. Give every worker **one** distinct subsystem with an exact, non-overlapping ownership boundary.
|
||||
|
||||
Keep concurrency bounded to the number of lanes you can actually coordinate. Use one consolidated wait. Do not interrupt productive workers just because they are slow. Close completed workers after harvesting results.
|
||||
|
||||
Each worker receives this brief:
|
||||
|
||||
> Review the assigned subsystem for **at most two** materially useful simplifications in its data structures, state representation, or organizing model.
|
||||
>
|
||||
> Inspect implementation, public interfaces, major call sites, and existing tests. Stay inside the assigned ownership boundary. You may name a cross-subsystem concern; do not expand scope to solve it.
|
||||
>
|
||||
> Look for:
|
||||
>
|
||||
> - scattered booleans or nullable fields that permit invalid combinations and should become a state machine or discriminated union
|
||||
> - repeated assumptions about object shape that need a shared typed model
|
||||
> - duplicated branching that a small map, registry, reducer, or command model would remove
|
||||
> - unclear state or behavior ownership that a small module boundary would clarify
|
||||
> - repeated scans, transformations, or lookups where a more appropriate collection or index would materially simplify behavior
|
||||
> - lifecycle, concurrency, or async states whose representation permits stale or contradictory state
|
||||
>
|
||||
> Do not force an abstraction. Prefer boring local code when it is already clear.
|
||||
>
|
||||
> Do not recommend changes solely for stylistic consistency, hypothetical extensibility, minor line-count reduction, or moving existing branching behind a new type.
|
||||
>
|
||||
> Return at most two opportunities. If nothing clearly meets the threshold, return `skip`.
|
||||
>
|
||||
> For every recommendation, provide:
|
||||
>
|
||||
> 1. Verdict: recommend or skip
|
||||
> 2. Evidence with exact file and line references
|
||||
> 3. Current complexity or invalid states
|
||||
> 4. Proposed representation and why it is simpler
|
||||
> 5. Smallest credible implementation scope, including affected files and interfaces
|
||||
> 6. Regression risks and migration concerns
|
||||
> 7. Existing and additional validation required
|
||||
> 8. Confidence: high, medium, or low
|
||||
|
||||
## 3. Validate and synthesize
|
||||
|
||||
Independently verify every finding against the current repository before accepting it.
|
||||
|
||||
Reject, narrow, or demote recommendations that are vague, duplicate another finding, misunderstand intentional Laravel/framework semantics, or merely relocate complexity.
|
||||
|
||||
Record skips as completed coverage. Deduplicate overlapping findings and assign each accepted recommendation to one authoritative subsystem.
|
||||
|
||||
Continue opening bounded review batches until every inventory row is complete.
|
||||
|
||||
## 4. Audit the audit
|
||||
|
||||
Before finishing, run fresh independent passes for:
|
||||
|
||||
- repository coverage and missing subsystem boundaries
|
||||
- duplication and ownership overlap
|
||||
- materiality and over-abstraction
|
||||
- schema completeness
|
||||
- dependency-aware priority ranking
|
||||
|
||||
If the coverage pass finds a real omission, add an explicit subsystem row and audit it. Do not hide it by broadening a previously completed boundary.
|
||||
|
||||
Rank the final recommendations by concrete impact, confidence, implementation effort, blast radius, and prerequisites. Identify the best first implementation slices.
|
||||
|
||||
Then assign each accepted finding a tier (put `metadata.priority` on the finding). Every promoted ID appears exactly once.
|
||||
|
||||
- **P0** — reachable wrong-record, lost-update, authorization, durable-state, or permanently incomplete-operation risks
|
||||
- **P1** — concrete boundary failures and high-leverage ownership fixes (less immediate damage, or greater migration cost)
|
||||
- **P2** — useful invariant improvements with narrower impact or sensitive migration
|
||||
- **P3** — telemetry / diagnostics / maintainability; keep this tier small after materiality review
|
||||
|
||||
Then render:
|
||||
|
||||
```bash
|
||||
php artisan auditor:report --findings=storage/auditor-findings.json
|
||||
```
|
||||
|
||||
The report includes a priority synthesis. Set `metadata.priority` to `p0`–`p3` when you rank explicitly.
|
||||
|
||||
## Done only when
|
||||
|
||||
- every identifiable subsystem has been reviewed
|
||||
- every subsystem has a recommendation or explicit skip
|
||||
- every finding has complete evidence, scope, risk, and validation fields
|
||||
- duplicates and weak abstractions have been removed
|
||||
- priorities and dependencies are internally consistent
|
||||
- the repository remains unchanged
|
||||
183
.github/skills/laravel-audit-performance/SKILL.md
vendored
Normal file
183
.github/skills/laravel-audit-performance/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
---
|
||||
name: laravel-audit-performance
|
||||
description: >
|
||||
Audit a Laravel application's performance deeply and safely: unnecessary
|
||||
queries, N+1, collection vs database work, memory amplification, repeated
|
||||
computation, I/O, queues, and rendering cost — verifying that every proposed
|
||||
optimization preserves behavior before reporting it. Use when auditing
|
||||
performance or investigating a slow path.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Performance Audit
|
||||
|
||||
Audit how the application spends time, memory, queries, and I/O — and report only optimizations that are **semantically verified** and **worth acting on**.
|
||||
|
||||
List applicable rules first:
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules --domain=performance --applicable
|
||||
```
|
||||
|
||||
Ecosystem rules (`AUD-LW-003`, `AUD-FIL-003`, `AUD-IN-003`) appear only when those packages are installed.
|
||||
|
||||
## The methodology
|
||||
|
||||
Work every candidate through this pipeline. A pattern that stops at any stage is not a finding.
|
||||
|
||||
```text
|
||||
Signal A suspicious shape (query in a loop, get()->count(), unbounded all())
|
||||
↓
|
||||
Context Who calls this? What route/job/command? What data volume? What else uses the result?
|
||||
↓
|
||||
Behavior What does the code actually do — accessors, casts, custom collections, callbacks?
|
||||
↓
|
||||
Verification Is the proposed replacement semantically equivalent FOR THIS USAGE?
|
||||
↓
|
||||
Impact Which resource does it save (queries, rows transferred, memory, CPU, I/O)? Is it meaningful?
|
||||
↓
|
||||
Finding Evidence-backed report with mechanism-based impact and justified severity
|
||||
```
|
||||
|
||||
**A suspicious pattern is not automatically a performance bug.**
|
||||
|
||||
### Worked example — when it IS a finding
|
||||
|
||||
```php
|
||||
$total = User::where('active', true)->get()->count();
|
||||
```
|
||||
|
||||
The collection is materialized only to be collapsed into one number. Every matching row is transferred and hydrated to produce what `User::where('active', true)->count()` returns as a single value. Verified: `$total` is a scalar; no other code uses the collection. Report via `AUD-PER-008`.
|
||||
|
||||
### Counter-example — same shape, NOT a finding
|
||||
|
||||
```php
|
||||
$users = User::where('active', true)->get();
|
||||
|
||||
$count = $users->count();
|
||||
|
||||
return view('users.index', compact('users', 'count'));
|
||||
```
|
||||
|
||||
Do **not** recommend `->count()` here. The collection is rendered by the view; replacing the aggregate would issue a second query or force a refactor. Deriving the count from already-loaded data is the cheapest correct option. This rule applies to every "obvious" optimization in this skill: check what else consumes the value first.
|
||||
|
||||
## What to investigate
|
||||
|
||||
Work through the areas relevant to this application. Use context tools (`routes`, `models`, `database_schema`, `jobs_events_schedules`) to find hot paths and data-volume sources before reading code.
|
||||
|
||||
### Database and Eloquent
|
||||
|
||||
- Materialized aggregates: `get()->count()/sum()/avg()/min()/max()`, `get()->isEmpty()/isNotEmpty()`, `get()->first()` where the collection has no other consumer (`AUD-PER-008`).
|
||||
- PHP doing database work: filtering, sorting, slicing, or deduplication of freshly queried rows that SQL could do (`AUD-PER-009`).
|
||||
- Relationship counts/existence answered by loading rows: `$post->comments->count()`, `->isNotEmpty()` vs `withCount()`/`withExists()`/`->relation()->count()` (`AUD-PER-010`).
|
||||
- Queries inside loops: per-item lookups, counts, writes without bulk forms (`AUD-PER-011`).
|
||||
- N+1 relationship access in loops, views, resources, jobs — verify the relationship is actually loaded lazily before reporting (`AUD-PER-001`).
|
||||
- Over-selection: full hydration of wide tables where few columns are consumed (`AUD-PER-013`).
|
||||
- Repeated identical queries within one lifecycle that can share one result (`AUD-PER-004`).
|
||||
|
||||
### Collections
|
||||
|
||||
- Chained transformations creating avoidable intermediates on large sets: `map()->filter()`, `filter()->first()` where `first(callable)` is equivalent (`AUD-PER-018`).
|
||||
- Multiple separate passes over one large collection where a single pass or a keyed lookup would do.
|
||||
- Semantic traps: `all()` returns the underlying items; `toArray()` recursively converts Arrayable items. They are interchangeable **only** when consumers tolerate the difference. Never swap them blindly.
|
||||
- Lazy vs eager: long chains over large datasets may suit `lazy()` collections — but only propose this when the chain is genuinely hot.
|
||||
|
||||
### Application flow
|
||||
|
||||
- Expensive synchronous work in request paths where a queue is clearly idiomatic (`AUD-PER-002`, `AUD-PER-005`).
|
||||
- Repeated expensive stable computations without a justified cache (`AUD-PER-007`).
|
||||
- Hot paths first: high-traffic routes, scheduled commands, queue workers. A slow admin page once a month is rarely worth the same attention.
|
||||
|
||||
### Data volume
|
||||
|
||||
- Unbounded retrieval on growth-prone paths: APIs, exports, reports, commands, jobs (`AUD-PER-012`). Match the mechanism to the workload: paginate/cursorPaginate for HTTP, chunkById/lazyById for read-modify-write, lazy/cursor for single-pass streaming.
|
||||
- Jobs processing unbounded datasets or carrying oversized serialized payloads (`AUD-PER-016`).
|
||||
|
||||
### I/O
|
||||
|
||||
- External HTTP calls inside loops, repeated identical requests, sequential independent calls that could batch (`AUD-PER-014`). Respect call dependencies and rate limits.
|
||||
- Storage/filesystem work repeated per item, or whole-file reads where streaming fits (`AUD-PER-015`). Remote disks make every call network I/O.
|
||||
|
||||
### Rendering
|
||||
|
||||
- Queries inside Blade loops/components, expensive helpers recomputed per iteration, stable values recomputed per render without caching (`AUD-PER-017`).
|
||||
- Livewire: per-render/update queries, computed properties that re-query on each subsequent update (they are memoized within a single request), oversized public properties serialized every cycle (`AUD-LW-003`).
|
||||
- Filament: select options loading whole tables, per-row closures querying, counts without `withCount` (`AUD-FIL-003`).
|
||||
- Inertia: shared props computed eagerly on every response including pages that never use them (`AUD-IN-003`).
|
||||
|
||||
Only investigate ecosystem areas when the package is installed.
|
||||
|
||||
## Verify semantic equivalence before recommending
|
||||
|
||||
Every recommendation must answer: **does the replacement produce identical observable results in this exact usage?** Check specifically:
|
||||
|
||||
- **Collection reuse** — is the materialized collection used anywhere else (view, return value, later loop)? If yes, most query-side rewrites do not apply.
|
||||
- **Accessors, casts, appended attributes** — PHP-side operations may see transformed values SQL never produces. Aggregate comparisons can differ.
|
||||
- **Comparison strictness** — Collection `where()`/`contains()` compare loosely by default; SQL compares differently. Null handling differs too.
|
||||
- **Ordering semantics** — collation, locale-aware sort callbacks, stability of sorts.
|
||||
- **Custom collection classes** — overridden methods (`count()`, `contains()`, `first()`) may have non-standard behavior.
|
||||
- **Model requirements** — narrowed selects still need primary keys, foreign keys, and anything accessors/casts touch.
|
||||
- **Side effects** — bulk writes skip model events unless explicitly handled; lazy loading timing changes when queries execute.
|
||||
|
||||
If equivalence cannot be established, either lower confidence and say so, or do not report.
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
Every performance finding needs at least:
|
||||
|
||||
- The specific site (file + line/range) and the enclosing method/route/job.
|
||||
- The cost driver with a source: iteration count origin, table growth expectation, call frequency — from routes/schema/jobs context, not vibes.
|
||||
- Why the current form performs the unnecessary work (what is transferred/computed and then discarded or repeated).
|
||||
- The verification note: what you checked to confirm the replacement is safe *here*.
|
||||
|
||||
Do not invent measurements. Describe mechanisms ("avoids transferring every matching row into PHP") rather than fabricating multipliers. Cite runtime evidence only if it actually exists.
|
||||
|
||||
## Severity guidance
|
||||
|
||||
Justify severity from reach × frequency × amplification:
|
||||
|
||||
- `high`: query explosion or heavy amplification on hot/high-volume paths (per-row queries over user-driven loops, unbounded retrieval on public endpoints, external calls multiplied by dataset size) — `AUD-PER-011` findings typically land here when loops are data-driven.
|
||||
- `medium`: clear unnecessary database work, avoidable materialization, repeated queries, oversized payloads, rendering-path queries with real traffic.
|
||||
- `low`: real but small inefficiencies — narrow-column wins, single-model count forms, pass-collapsing on modest collections.
|
||||
- `info`: observations worth noting without action.
|
||||
|
||||
A correct micro-optimization on a tiny dataset is not a finding at all.
|
||||
|
||||
## False positives — keep the report trustworthy
|
||||
|
||||
- The collection/result is reused after the suspicious call (the counter-example above).
|
||||
- Semantics depend on PHP-only behavior the replacement cannot reproduce.
|
||||
- Loops bounded by small fixed sets (config constants, enum values).
|
||||
- Inherently bounded tables (countries, settings) need no pagination.
|
||||
- Already-loaded relationships queried again would be *slower*, not faster.
|
||||
- Caching without a key/invalidation story; concurrency for dependent requests; eager loading everything "just in case" (over-eager loading is itself waste).
|
||||
|
||||
## Performance checklist
|
||||
|
||||
Run this checklist over each scoped area. Every checked box needs evidence before it becomes a finding.
|
||||
|
||||
```text
|
||||
□ Are queries executed unnecessarily?
|
||||
□ Are queries executed inside loops?
|
||||
□ Are relationships causing N+1 queries?
|
||||
□ Is data loaded that is never needed?
|
||||
□ Is a collection materialized unnecessarily?
|
||||
□ Is SQL work being performed in PHP?
|
||||
□ Is PHP work being performed where SQL is more appropriate?
|
||||
□ Are aggregates done in PHP unnecessarily?
|
||||
□ Are large datasets loaded into memory?
|
||||
□ Is pagination/chunking/lazy iteration appropriate?
|
||||
□ Are unnecessary columns selected?
|
||||
□ Are identical queries repeated?
|
||||
□ Are external HTTP calls repeated?
|
||||
□ Are Storage/filesystem operations repeated?
|
||||
□ Are jobs processing too much data?
|
||||
□ Is rendering causing repeated queries?
|
||||
□ Is Livewire state unnecessarily large?
|
||||
□ Is Filament performing repeated database work?
|
||||
□ Is the suggested optimization semantically equivalent?
|
||||
□ Is the optimization meaningful enough to report?
|
||||
```
|
||||
|
||||
The last two boxes gate everything above them.
|
||||
63
.github/skills/laravel-audit-security/SKILL.md
vendored
Normal file
63
.github/skills/laravel-audit-security/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
name: laravel-audit-security
|
||||
description: >
|
||||
Audit a Laravel application's security boundaries: authorization, mass
|
||||
assignment, sensitive data, file/URL handling, CSRF, XSS, injection, and
|
||||
secrets. Use when auditing security or when asked to review security of a
|
||||
Laravel app.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Security Audit
|
||||
|
||||
Audit the security boundaries of the Laravel application. **Do not invent vulnerabilities merely because a pattern is uncommon.** Require context and evidence.
|
||||
|
||||
List applicable rules first:
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules --domain=security --applicable
|
||||
```
|
||||
|
||||
## What to look for
|
||||
|
||||
- Authorization gaps: routes/controllers that mutate or expose resources without a policy, gate, or middleware.
|
||||
- Insecure resource access: missing ownership checks on update/delete/show.
|
||||
- Missing policies/gates where the app has clear resource ownership.
|
||||
- Mass assignment risks: unguarded models or over-permissive `$fillable` combined with direct request input.
|
||||
- Unsafe validation assumptions: trusting `required`/client-side checks, missing server-side validation on critical fields.
|
||||
- Sensitive data exposure: secrets or personal data in responses, logs, exceptions, or dumps.
|
||||
- Unsafe configuration usage: `APP_DEBUG` assumptions, hardcoded credentials, env values echoed.
|
||||
- Dangerous file handling: unvalidated uploads, paths from user input, unsafe file reads.
|
||||
- Unsafe URL/redirect handling: open redirects from user-controlled targets.
|
||||
- Insecure authentication/authorization patterns: storing secrets in plaintext, weak session config, missing rate limits on auth.
|
||||
- Secrets accidentally committed or exposed where detectable (`.env` committed, keys in source).
|
||||
- Weak or missing CSRF protection: `VerifyCsrfToken::except` whitelists, removed/reordered CSRF middleware, forms or post endpoints missing the token.
|
||||
- Unescaped output / XSS risk: Blade `{!! !!}`, `->get()`, `->toHtml()` rendering user-controlled or stored data.
|
||||
- Raw SQL with user-controlled input: `DB::raw`, `whereRaw`, `selectRaw`, `orderByRaw`, or `statement` interpolating request input.
|
||||
- Known vulnerable dependencies. `dependencies.composer_audit` is **on by default**. Use it for `AUD-DEP-001` when `available` is true. If `available` is false, read `reason` or run `composer audit --format=json` yourself — do not treat an empty payload as “no advisories”. Set `laravel-auditor.context.composer_audit` to `false` only when you need to skip the shell-out.
|
||||
- Queued jobs that serialize Eloquent models with hidden/sensitive attributes (`AUD-QUE-002`).
|
||||
|
||||
When these packages are installed, also apply their packs (`auditor:rules --applicable`): Livewire (`AUD-LW-*`), Filament (`AUD-FIL-*`), Inertia (`AUD-IN-*`), Sanctum (`AUD-API-*`).
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
Every security finding needs at least:
|
||||
|
||||
- The specific route/controller/model file and line/range.
|
||||
- The relevant middleware/policy/gate (or the absence of one).
|
||||
- What the attacker-controlled input is, when applicable.
|
||||
|
||||
## False positives
|
||||
|
||||
- Do not claim exploitability without sufficient evidence.
|
||||
- A missing policy is only a finding when the resource is user-accessible or sensitive.
|
||||
- Uncommon patterns are not automatically vulnerabilities.
|
||||
- An empty `composer_audit` payload is not evidence that there are no advisories unless the check actually ran.
|
||||
|
||||
## Severity guidance
|
||||
|
||||
- `critical`: directly exploitable, e.g. unauthenticated access to sensitive data or arbitrary code execution.
|
||||
- `high`: requires a real but plausible precondition (e.g. any authenticated user).
|
||||
- `medium`/`low`: defense-in-depth issues, configuration concerns, or low-exploitability risks.
|
||||
- `info`: observations worth documenting without immediate risk.
|
||||
50
.github/skills/laravel-audit-testing/SKILL.md
vendored
Normal file
50
.github/skills/laravel-audit-testing/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
name: laravel-audit-testing
|
||||
description: >
|
||||
Audit a Laravel application's test suite: meaningful coverage, authorization
|
||||
tests, edge cases, and Pest/PHPUnit conventions. Use when auditing testing.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Testing Audit
|
||||
|
||||
Audit the test suite of the Laravel application. **Do not equate line coverage with test quality.**
|
||||
|
||||
List applicable rules first:
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules --domain=testing --applicable
|
||||
```
|
||||
|
||||
## What to look for
|
||||
|
||||
- Critical business flows with little or no test coverage: purchases, authorization boundaries, data mutations, integrations.
|
||||
- Tests that do not verify meaningful behavior: tests that only assert the request succeeded, tests that pin implementation details, or tests that mock everything away.
|
||||
- Missing authorization/security tests around critical boundaries: no test proving an unauthorized user is rejected.
|
||||
- Important edge cases left untested: boundary values, empty states, duplicates, permissions variations.
|
||||
- Brittle testing patterns: tests coupled to exact query counts, implementation ordering, or unrelated details.
|
||||
- Excessive implementation-detail testing: tests that break on any refactor without protecting behavior.
|
||||
- Inconsistent use of Pest/PHPUnit conventions across the suite.
|
||||
- In a Pest-first suite (`tests/Pest.php` or `pestphp/pest`), new PHPUnit class tests without a documented reason (`AUD-PEST-001`).
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
Every testing finding needs at least:
|
||||
|
||||
- The critical flow or boundary (file + route/controller/service).
|
||||
- The relevant test files that should cover it.
|
||||
- What behavior is or is not asserted.
|
||||
|
||||
## False positives
|
||||
|
||||
- Coverage may exist under a different name or location — check before reporting.
|
||||
- A missing test for an unimportant flow is not a finding.
|
||||
- Do not report line-coverage numbers as proof of quality (or lack of it).
|
||||
|
||||
## Severity guidance
|
||||
|
||||
- `high`: a critical boundary (e.g. authorization) has no meaningful tests.
|
||||
- `medium`: an important flow lacks meaningful coverage or tests are brittle.
|
||||
- `low`: testing hygiene issues.
|
||||
- `info`: observations.
|
||||
134
.github/skills/laravel-audit/SKILL.md
vendored
Normal file
134
.github/skills/laravel-audit/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
---
|
||||
name: laravel-audit
|
||||
description: >
|
||||
Perform a deep, structured, evidence-based audit of a Laravel application
|
||||
and produce actionable findings backed by concrete project evidence. Use when
|
||||
asked to audit, review, assess, or evaluate an existing Laravel codebase.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Audit
|
||||
|
||||
Perform a deep, structured, evidence-based audit of the current Laravel application. Your goal is **trustworthy findings backed by concrete evidence**, not a long list of style nitpicks.
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when asked to audit, review, assess, or evaluate an existing Laravel application for security, performance, architecture, database, testing, or Laravel conventions issues.
|
||||
|
||||
For a bounded data-structure / ownership / organizing-model pass (inventory every subsystem, fan out read-only workers, validate and rank P0–P3), use the `laravel-audit-dsa` skill instead.
|
||||
|
||||
## Core principles
|
||||
|
||||
- **Evidence-first.** Every meaningful finding must cite concrete, verifiable project context: a file path, line/range, symbol, route, migration, query, configuration key, or dependency.
|
||||
- **Deterministic facts over guesses.** Prefer programmatically gathered facts (Artisan/MCP context tools) over reading raw text and inferring.
|
||||
- **No invented behavior.** Never claim a package or framework behaves a certain way without verifying it. When uncertain, say the evidence is incomplete.
|
||||
- **Confirmed vs. hypothesis.** Label findings by confidence: `confirmed` only when evidence fully supports it; otherwise `high`, `medium`, or `low`.
|
||||
- **Trustworthiness over volume.** A few high-quality findings beat dozens of speculative ones.
|
||||
- **Read-only.** Never modify application code during the audit.
|
||||
- **No severity inflation.** Style preferences are never high severity. "Package is old" is not a finding by itself.
|
||||
|
||||
## Audit workflow
|
||||
|
||||
### Phase A: Discover
|
||||
|
||||
Gather deterministic project facts first. When Artisan is available, start with `php artisan auditor:status`, `php artisan auditor:context --list`, and `php artisan auditor:rules --applicable`. Then run the Laravel Auditor context tools (MCP) or inspect:
|
||||
|
||||
- `project_info`: Laravel version, PHP version, database engine, ecosystem packages, architecture signals.
|
||||
- `routes`: registered routes and their handlers.
|
||||
- `models`: models, tables, fillable/guarded, casts, relationships.
|
||||
- `database_schema`: tables, columns, types, indexes (read-only).
|
||||
- `dependencies`: composer packages and versions.
|
||||
- `configuration`: config keys and safe values.
|
||||
- `policies_authorization`: gates, policies, middleware.
|
||||
- `jobs_events_schedules`: jobs, events/listeners, schedules.
|
||||
- `tests`: test framework and coverage signals.
|
||||
- `migrations`: migration files.
|
||||
- `subsystems`: ownership-bounded inventory for a DSA-style coordinator audit.
|
||||
|
||||
Four tools accept optional read-only filters for focused verification: `routes` (`uri`, `name`, `action`, `method`), `models` (`class`, `table`), `database_schema` (`table`), and `dependencies` (`package`). Filtered responses report `total_count` so you always know how much of the full inventory was returned; call without arguments for the complete payload.
|
||||
|
||||
Fall back to `composer.json`, `bootstrap/app.php`, `config/app.php`, and the file tree when tools are unavailable.
|
||||
|
||||
Record the application type (web, API, admin panel, package) and any ecosystem packages (Livewire, Filament, Inertia, Pest, Sanctum, Horizon, etc.) — these determine which rules apply.
|
||||
|
||||
Build a **feature inventory** from the route surface and UI entry points before scoping. List each feature (login, checkout, admin dashboard, etc.), the routes and views that implement it, and the model/service behind it. This inventory drives testing and authorization coverage later, and surfaces stubbed or hallucinated features (a route pointing at a missing controller, an empty view, a TODO handler) early.
|
||||
|
||||
### Phase B: Scope
|
||||
|
||||
Select only the audit domains relevant to this application. Do not blindly run every check. Reason about which domains matter and state the scope before investigating.
|
||||
|
||||
Default domains: `security`, `performance`, `architecture`, `database`, `testing`, `conventions`. Skip domains that are clearly irrelevant (e.g. skip queue analysis when the app has no jobs or queue driver).
|
||||
|
||||
### Phase C: Investigate
|
||||
|
||||
For each selected domain:
|
||||
|
||||
1. Collect relevant project facts using the context tools.
|
||||
2. Inspect source code around the facts.
|
||||
3. Trace important behavior across files (controller → service → model → route → view).
|
||||
4. Cross-check evidence across multiple sources.
|
||||
5. Separate confirmed problems from hypotheses.
|
||||
|
||||
The rule reference (`php artisan auditor:rules`, or `--applicable` / `--domain=`) contains the rule IDs, severity, evidence requirements, and false-positive considerations. Use it to map observations to stable rule IDs. Ecosystem packs (Livewire, Filament, Inertia, Sanctum, Pest) only apply when those packages are installed. Queue and DSA rules always apply.
|
||||
|
||||
### Phase D: Verify
|
||||
|
||||
Before reporting a high-severity finding, attempt to verify it:
|
||||
|
||||
- Inspect the route and its middleware.
|
||||
- Inspect the model/relationship definitions.
|
||||
- Inspect the schema or migrations.
|
||||
- Inspect the config for the relevant keys.
|
||||
- Inspect dependency versions and their docs.
|
||||
- Inspect the tests.
|
||||
- Review logs/errors when relevant.
|
||||
- Run a **safe, read-only** runtime check only when a tool exists (never migrate, seed, refresh, or otherwise mutate data).
|
||||
|
||||
If you cannot verify, lower the confidence and say so explicitly.
|
||||
|
||||
### Phase E: Report
|
||||
|
||||
Write findings to `storage/auditor-findings.json` (an array, or `{ "findings": [ ... ] }`). Each finding must include:
|
||||
|
||||
- `id`: unique instance id (e.g. `F-2026-0001`).
|
||||
- `rule_id`: a stable rule ID (e.g. `AUD-SEC-001`) when one matches.
|
||||
- `title`: short and specific.
|
||||
- `domain`: the audit domain.
|
||||
- `severity`: `critical`, `high`, `medium`, `low`, or `info`.
|
||||
- `confidence`: `confirmed`, `high`, `medium`, or `low`.
|
||||
- `status`: `open` for new findings.
|
||||
- `summary`: what is wrong.
|
||||
- `why_it_matters`: why it matters for this app.
|
||||
- `evidence`: concrete references (file paths, lines, routes, symbols).
|
||||
- `affected_resources`: files/routes/config involved.
|
||||
- `recommendation`: what to do about it.
|
||||
- `remediation`: optional step-by-step guidance.
|
||||
- `verification_notes`: how you verified (or why you could not).
|
||||
- `metadata.priority`: `p0`–`p3` when you rank the report. Keep P3 small.
|
||||
- **P0** — reachable wrong-record, lost-update, authorization, or durable-state risk
|
||||
- **P1** — concrete boundary failures with less immediate damage
|
||||
- **P2** — useful invariant improvements with narrower impact
|
||||
- **P3** — telemetry / diagnostics / maintainability
|
||||
|
||||
Then render:
|
||||
|
||||
```bash
|
||||
php artisan auditor:report --findings=storage/auditor-findings.json
|
||||
php artisan auditor:ci --findings=storage/auditor-findings.json --fail-on=high
|
||||
```
|
||||
|
||||
The report includes project facts, domains audited, counts by severity/domain, priority synthesis, and the key risks.
|
||||
|
||||
## Anti-patterns to avoid
|
||||
|
||||
- Inventing vulnerabilities because a pattern is uncommon.
|
||||
- Treating every loop or every query as a performance problem.
|
||||
- Recommending "create a repository for every model" or "move everything into services".
|
||||
- Equating line coverage with test quality.
|
||||
- Claiming exploitability without evidence.
|
||||
- Recommending upgrades solely because a package is old.
|
||||
- Presenting style preferences as high-severity findings.
|
||||
- Claiming the audit is complete when evidence was missing.
|
||||
- Running `migrate`, `db:wipe`, `db:seed`, or any other mutating Artisan command to "verify" schema.
|
||||
- Treating `composer_audit.available: false` (or an empty advisory list) as “no vulnerabilities” when the collector did not actually run `composer audit`.
|
||||
136
.github/skills/laravel-auditor-development/SKILL.md
vendored
Normal file
136
.github/skills/laravel-auditor-development/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
name: laravel-auditor-development
|
||||
description: >
|
||||
Integrate and apply the Laravel Auditor package in Laravel applications:
|
||||
installation, setup, the MCP tools, and the audit workflow. Use when setting
|
||||
up Laravel Auditor or wiring it into a Laravel app.
|
||||
metadata:
|
||||
agent: any
|
||||
---
|
||||
|
||||
# Laravel Auditor Development
|
||||
|
||||
Use this skill when a Laravel application needs to install, configure, or apply the Laravel Auditor package. Auditor does not scan the app. After setup, the user asks their AI agent to use the `laravel-audit` skill.
|
||||
|
||||
## Primary goals
|
||||
|
||||
- Install Laravel Auditor as a development dependency.
|
||||
- Run the appropriate setup command (Boost-based or standalone).
|
||||
- Verify setup with the diagnostics command.
|
||||
- Use the audit workflow skill to perform an audit.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Install the package
|
||||
|
||||
```bash
|
||||
composer require --dev mrpunyapal/laravel-auditor
|
||||
```
|
||||
|
||||
### 2. Set up resources
|
||||
|
||||
When Laravel Boost is installed, expose Auditor resources through Boost:
|
||||
|
||||
```bash
|
||||
php artisan boost:install
|
||||
```
|
||||
|
||||
(or `php artisan boost:update` after package updates).
|
||||
|
||||
When Boost is not installed, use the standalone installer:
|
||||
|
||||
```bash
|
||||
php artisan auditor:install --agents=claude_code
|
||||
php artisan auditor:install --agents=claude_code,opencode
|
||||
```
|
||||
|
||||
The installer is idempotent and safe. It detects the Laravel application context, detects whether Boost is installed, prepares Auditor's agent-facing resources, and reports what it created or updated.
|
||||
|
||||
Non-interactive runs resolve agents from `--agents`, then `laravel-auditor.agents`, then project markers. When none of those resolve, no agents are wired. A `.github` directory alone is not treated as Copilot. Pass `--force` to refresh Auditor-owned resources, including an existing `laravel-auditor` MCP entry.
|
||||
|
||||
### 3. Verify setup
|
||||
|
||||
```bash
|
||||
php artisan auditor:status
|
||||
```
|
||||
|
||||
### 4. Inspect the available rules and project facts
|
||||
|
||||
```bash
|
||||
php artisan auditor:rules
|
||||
php artisan auditor:context --list
|
||||
php artisan auditor:context project_info
|
||||
```
|
||||
|
||||
### 5. Run an audit
|
||||
|
||||
Ask the agent to use the `laravel-audit` skill to perform a structured, evidence-based audit of the application.
|
||||
|
||||
### 6. Render a report
|
||||
|
||||
If the agent produced a JSON findings file:
|
||||
|
||||
```bash
|
||||
php artisan auditor:report --example
|
||||
php artisan auditor:report --findings=storage/auditor-findings.json --format=markdown
|
||||
```
|
||||
|
||||
Supported formats: `markdown`, `json`, `text`, `sarif`. Finding schema: publish tag `laravel-auditor-schema`.
|
||||
|
||||
### 7. Optional MCP
|
||||
|
||||
```bash
|
||||
php artisan auditor:mcp -q
|
||||
```
|
||||
|
||||
Register that stdio command with the agent so it can call `project_info`, `routes`, `models`, `migrations`, `database_schema`, `dependencies`, `configuration`, `policies_authorization`, `jobs_events_schedules`, `tests`, and `subsystems`. Use `-q` so Artisan boot output cannot break MCP stdio framing.
|
||||
|
||||
With Laravel Boost installed, the same context tools are registered automatically inside Boost's MCP server (via `boost.mcp.tools.include`); no extra setup is needed.
|
||||
|
||||
## References
|
||||
|
||||
- Install: `composer require --dev mrpunyapal/laravel-auditor`
|
||||
- Boost path: `php artisan boost:install` / `php artisan boost:update`
|
||||
- Standalone path: `php artisan auditor:install` (`--dry-run`, `--force`, `--agents=`)
|
||||
- Diagnostics: `php artisan auditor:status`
|
||||
- Rules: `php artisan auditor:rules` (`--domain=`, `--json`, `--applicable`)
|
||||
- Context: `php artisan auditor:context` (`--list`, `{collector}`, `--output=`)
|
||||
- Reports: `php artisan auditor:report` (`--findings=`, `--example`, `--format=markdown|json|text|sarif`, `--output=`)
|
||||
- CI: `php artisan auditor:ci --findings=storage/auditor-findings.json --fail-on=high`
|
||||
- Facade: `LaravelAuditor::collect('routes')`, `LaravelAuditor::rules()`, `LaravelAuditor::context()`, `LaravelAuditor::project()`
|
||||
- Config: `resources_target`, `agents`, `context.composer_audit` (on), `context.test_listing` (off)
|
||||
- Config publish tag: `laravel-auditor-config`
|
||||
- Resource publish tag: `laravel-auditor-resources`
|
||||
- Schema publish tag: `laravel-auditor-schema`
|
||||
- Example publish tag: `laravel-auditor-examples`
|
||||
- Audit workflow skill: `laravel-audit` (`laravel-audit-dsa` for subsystem / P0–P3 ranking)
|
||||
|
||||
## Examples
|
||||
|
||||
Ask the agent:
|
||||
|
||||
> Use the laravel-audit skill. Discover this Laravel app, scope the relevant domains, and report only findings with file, route, or schema evidence.
|
||||
|
||||
A finding should include `rule_id`, `severity`, `confidence`, `summary`, `why_it_matters`, `evidence`, and `recommendation`.
|
||||
|
||||
Full audit prompt:
|
||||
|
||||
> You are auditing the Laravel application in this project using the Laravel Auditor methodology.
|
||||
>
|
||||
> 1. Use the laravel-audit skill. Follow its Discover → Scope → Verify → Report workflow.
|
||||
> 2. Start by calling the context MCP tools to gather deterministic facts BEFORE reading code: `project_info`, `routes`, `models`, `migrations`, `database_schema`, `dependencies`, `configuration`, `policies_authorization`, `jobs_events_schedules`, `tests`, `subsystems`.
|
||||
> 3. Scope the relevant domains (security, database, architecture, testing, ...). Pick the domains with the most risk signal and go deep.
|
||||
> 4. For every potential finding, verify against actual files, routes, or schema. Never report a guess.
|
||||
> 5. Report findings ranked P0–P3, each with: file/route/schema evidence, the rule violated, why it matters, and a concrete fix.
|
||||
> 6. Be read-only. Do not modify any application code.
|
||||
|
||||
Discover-only quick pass:
|
||||
|
||||
> Start with a Discover phase only: run all 11 context tools, summarize what this app is (framework versions, database, route surface, model list, test coverage), and flag any immediate red flags in 3-5 bullets. Do not write findings yet.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Do not require Boost just to use Auditor.
|
||||
- Do not run `php artisan auditor:install` when Boost is installed unless a standalone path is explicitly wanted.
|
||||
- Do not modify application code during setup or during an audit.
|
||||
- Do not treat Auditor as an autonomous scanner that invents findings without evidence.
|
||||
59
.github/skills/laravel-best-practices/SKILL.md
vendored
Normal file
59
.github/skills/laravel-best-practices/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
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, organized as an index of rule files. Each rule file 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, and 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.
|
||||
|
||||
## How to Apply
|
||||
|
||||
1. Check the changed files, nearby code, project configuration, and relevant tests for established patterns. Deviate only for a correctness or security defect, and call the deviation out.
|
||||
2. Map every affected concern to the rule index below. Read each mapped rule file before editing. Skip unrelated rule files.
|
||||
3. Make the smallest coherent change. Keep the application's architecture and naming instead of introducing a second pattern for the same job.
|
||||
4. Verify version-sensitive Laravel APIs for the installed version with `search-docs`, or inspect the installed framework when it is unavailable.
|
||||
5. Run the narrowest relevant tests first, then the project's formatting and static-analysis checks when the change warrants them.
|
||||
6. Re-read the diff against every mapped rule before finishing.
|
||||
|
||||
## Rule Index
|
||||
|
||||
Cross-cutting changes often need more than one rule file.
|
||||
|
||||
| Concern | Read |
|
||||
| --- | --- |
|
||||
| Query count, eager loading, indexes, large datasets | [`rules/db-performance.md`](rules/db-performance.md) |
|
||||
| Subqueries, aggregates, complex ordering and query plans | [`rules/advanced-queries.md`](rules/advanced-queries.md) |
|
||||
| Models, relationships, scopes, casts | [`rules/eloquent.md`](rules/eloquent.md) |
|
||||
| Authentication, authorization, input safety, secrets, uploads | [`rules/security.md`](rules/security.md) |
|
||||
| Form Requests and validation rules | [`rules/validation.md`](rules/validation.md) |
|
||||
| Controllers, route binding, resources, middleware | [`rules/routing.md`](rules/routing.md) |
|
||||
| Schema changes, columns, foreign keys, indexes | [`rules/migrations.md`](rules/migrations.md) |
|
||||
| Jobs, retries, uniqueness, batches, Horizon | [`rules/queue-jobs.md`](rules/queue-jobs.md) |
|
||||
| Cache lifetime, invalidation, locks, memoization | [`rules/caching.md`](rules/caching.md) |
|
||||
| Outbound requests, retries, timeouts, fakes | [`rules/http-client.md`](rules/http-client.md) |
|
||||
| Exceptions, reporting, rendering, log context | [`rules/error-handling.md`](rules/error-handling.md) |
|
||||
| Events and notifications | [`rules/events-notifications.md`](rules/events-notifications.md) |
|
||||
| Mailables and mail assertions | [`rules/mail.md`](rules/mail.md) |
|
||||
| Scheduled tasks and overlap protection | [`rules/scheduling.md`](rules/scheduling.md) |
|
||||
| Collections, lazy iteration, bulk operations | [`rules/collections.md`](rules/collections.md) |
|
||||
| Blade components, attributes, composers | [`rules/blade-views.md`](rules/blade-views.md) |
|
||||
| Environment values and application configuration | [`rules/config.md`](rules/config.md) |
|
||||
| Tests: coverage, factories, fakes, and assertions | the `testing-best-practices` skill |
|
||||
| Naming, helpers, file boundaries, PHP style | [`rules/style.md`](rules/style.md) |
|
||||
| Actions, services, dependencies, application structure | [`rules/architecture.md`](rules/architecture.md) |
|
||||
|
||||
## Decision Rules
|
||||
|
||||
- Prefer framework features and existing application abstractions over new helpers or dependencies.
|
||||
- Avoid speculative abstractions. Extract code when it creates a clear domain boundary, removes meaningful duplication, or makes behavior independently testable.
|
||||
- Keep database access out of Blade views and prevent hidden N+1 queries across controllers, resources, jobs, and serialization.
|
||||
106
.github/skills/laravel-best-practices/rules/advanced-queries.md
vendored
Normal file
106
.github/skills/laravel-best-practices/rules/advanced-queries.md
vendored
Normal file
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
```
|
||||
206
.github/skills/laravel-best-practices/rules/architecture.md
vendored
Normal file
206
.github/skills/laravel-best-practices/rules/architecture.md
vendored
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
# 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 handle(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. Without an explicit `ORDER BY`, row order is undefined.
|
||||
|
||||
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, inside a transaction
|
||||
DB::transaction(function () use ($id) {
|
||||
$product = Product::where('id', $id)->lockForUpdate()->first();
|
||||
|
||||
// Read and update the product while the lock is held...
|
||||
});
|
||||
```
|
||||
|
||||
## 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);
|
||||
}
|
||||
}
|
||||
```
|
||||
36
.github/skills/laravel-best-practices/rules/blade-views.md
vendored
Normal file
36
.github/skills/laravel-best-practices/rules/blade-views.md
vendored
Normal file
|
|
@ -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
|
||||
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
|
||||
{{ $message }}
|
||||
</div>
|
||||
```
|
||||
|
||||
## 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.
|
||||
70
.github/skills/laravel-best-practices/rules/caching.md
vendored
Normal file
70
.github/skills/laravel-best-practices/rules/caching.md
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Caching Best Practices
|
||||
|
||||
## Use `Cache::remember()` Instead of Manual Get/Put
|
||||
|
||||
Cleaner cache-aside pattern that removes boilerplate. use `Cache::lock()` for race conditions.
|
||||
|
||||
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. Not supported by the `file`, `dynamodb`, `database` or `storage` drivers.
|
||||
|
||||
```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']],
|
||||
```
|
||||
44
.github/skills/laravel-best-practices/rules/collections.md
vendored
Normal file
44
.github/skills/laravel-best-practices/rules/collections.md
vendored
Normal file
|
|
@ -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 {}
|
||||
```
|
||||
73
.github/skills/laravel-best-practices/rules/config.md
vendored
Normal file
73
.github/skills/laravel-best-practices/rules/config.md
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Configuration Best Practices
|
||||
|
||||
## `env()` Only in Config Files
|
||||
|
||||
Direct `env()` calls may 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'));
|
||||
```
|
||||
192
.github/skills/laravel-best-practices/rules/db-performance.md
vendored
Normal file
192
.github/skills/laravel-best-practices/rules/db-performance.md
vendored
Normal file
|
|
@ -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
|
||||
```
|
||||
150
.github/skills/laravel-best-practices/rules/eloquent.md
vendored
Normal file
150
.github/skills/laravel-best-practices/rules/eloquent.md
vendored
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# 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
|
||||
#[Scope]
|
||||
protected function active(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
|
||||
#[Scope]
|
||||
protected function published(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.
|
||||
72
.github/skills/laravel-best-practices/rules/error-handling.md
vendored
Normal file
72
.github/skills/laravel-best-practices/rules/error-handling.md
vendored
Normal file
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
```
|
||||
52
.github/skills/laravel-best-practices/rules/events-notifications.md
vendored
Normal file
52
.github/skills/laravel-best-practices/rules/events-notifications.md
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# 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 — call `afterCommit()` to delay dispatch until the transaction commits.
|
||||
|
||||
```php
|
||||
$user->notify((new InvoicePaid($invoice))->afterCommit());
|
||||
```
|
||||
|
||||
## 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.
|
||||
160
.github/skills/laravel-best-practices/rules/http-client.md
vendored
Normal file
160
.github/skills/laravel-best-practices/rules/http-client.md
vendored
Normal file
|
|
@ -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.example.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.example.com/v1/charges', $data);
|
||||
```
|
||||
|
||||
Only retry on specific errors:
|
||||
|
||||
```php
|
||||
$response = Http::retry(3, 100, function (Throwable $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(),
|
||||
]);
|
||||
```
|
||||
27
.github/skills/laravel-best-practices/rules/mail.md
vendored
Normal file
27
.github/skills/laravel-best-practices/rules/mail.md
vendored
Normal file
|
|
@ -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 fail `assertSent` with a "Did you mean to use assertQueued()?" hint.
|
||||
|
||||
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.
|
||||
121
.github/skills/laravel-best-practices/rules/migrations.md
vendored
Normal file
121
.github/skills/laravel-best-practices/rules/migrations.md
vendored
Normal file
|
|
@ -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')->index()->constrained();
|
||||
$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']);
|
||||
```
|
||||
144
.github/skills/laravel-best-practices/rules/queue-jobs.md
vendored
Normal file
144
.github/skills/laravel-best-practices/rules/queue-jobs.md
vendored
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# 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(): \DateTimeInterface
|
||||
{
|
||||
return now()->addHours(4);
|
||||
}
|
||||
```
|
||||
|
||||
## Use `ShouldBeUniqueUntilProcessing` for Early Lock Release
|
||||
|
||||
`ShouldBeUnique` holds the lock until the job completes. `ShouldBeUniqueUntilProcessing` releases it when processing starts, allowing new instances to queue.
|
||||
|
||||
```php
|
||||
class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
|
||||
{
|
||||
// Lock releases when processing begins, not when it finishes
|
||||
}
|
||||
```
|
||||
|
||||
## 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,
|
||||
],
|
||||
],
|
||||
],
|
||||
```
|
||||
99
.github/skills/laravel-best-practices/rules/routing.md
vendored
Normal file
99
.github/skills/laravel-best-practices/rules/routing.md
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# 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);
|
||||
// In routes/api.php — the /api prefix is applied automatically
|
||||
Route::apiResource('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');
|
||||
}
|
||||
```
|
||||
39
.github/skills/laravel-best-practices/rules/scheduling.md
vendored
Normal file
39
.github/skills/laravel-best-practices/rules/scheduling.md
vendored
Normal file
|
|
@ -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');
|
||||
});
|
||||
```
|
||||
198
.github/skills/laravel-best-practices/rules/security.md
vendored
Normal file
198
.github/skills/laravel-best-practices/rules/security.md
vendored
Normal file
|
|
@ -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(UpdatePostRequest $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/PATCH/DELETE Blade forms. Inertia doesn't use `@csrf`; its HTTP client sends the `XSRF-TOKEN` cookie back as the `X-XSRF-TOKEN` header, which Laravel accepts in place of the `_token` field.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
<form method="POST" action="/posts">
|
||||
<input type="text" name="title">
|
||||
</form>
|
||||
```
|
||||
|
||||
Correct:
|
||||
```blade
|
||||
<form method="POST" action="/posts">
|
||||
@csrf
|
||||
<input type="text" name="title">
|
||||
</form>
|
||||
```
|
||||
|
||||
## 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 and size. Both `mimes` and `mimetypes` read the file's contents to guess its MIME type; `mimes` just expresses the allow-list as extensions. The `extensions` rule checks only the client-supplied filename, so never rely on it alone. 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
125
.github/skills/laravel-best-practices/rules/style.md
vendored
Normal file
125
.github/skills/laravel-best-practices/rules/style.md
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Conventions & Style
|
||||
|
||||
## Follow Laravel Naming Conventions
|
||||
|
||||
| What | Convention | Good | Bad |
|
||||
|------|-----------|------|-----|
|
||||
| Controller | singular | `ArticleController` | `ArticlesController` |
|
||||
| Model | singular | `User` | `Users` |
|
||||
| Table | plural, snake_case | `article_comments` | `articleComments` |
|
||||
| Pivot table | singular alphabetical | `article_user` | `user_article` |
|
||||
| Column | snake_case, no model name | `meta_title` | `article_meta_title` |
|
||||
| Foreign key | singular model + `_id` | `article_id` | `articles_id` |
|
||||
| Route | plural | `articles/1` | `article/1` |
|
||||
| Route name | snake_case with dots | `users.show_active` | `users.show-active` |
|
||||
| Method | camelCase | `getAll` | `get_all` |
|
||||
| Variable | camelCase | `$articlesWithAuthor` | `$articles_with_author` |
|
||||
| Collection | descriptive, plural | `$activeUsers` | `$data` |
|
||||
| Object | descriptive, singular | `$activeUser` | `$users` |
|
||||
| View | kebab-case | `show-filtered.blade.php` | `showFiltered.blade.php` |
|
||||
| Config | snake_case | `google_calendar.php` | `googleCalendar.php` |
|
||||
| Enum | singular | `UserType` | `UserTypes` |
|
||||
|
||||
## Prefer Shorter Readable Syntax
|
||||
|
||||
| Verbose | Shorter |
|
||||
|---------|---------|
|
||||
| `Session::get('cart')` | `session('cart')` |
|
||||
| `$request->session()->get('cart')` | `session('cart')` |
|
||||
| `$request->input('name')` | `$request->name` |
|
||||
| `return Redirect::back()` | `return back()` |
|
||||
| `Carbon::now()` | `now()` |
|
||||
| `App::make('Class')` | `app('Class')` |
|
||||
| `->where('column', '=', 1)` | `->where('column', 1)` |
|
||||
| `->orderBy('created_at', 'desc')` | `->latest()` |
|
||||
| `->orderBy('created_at', 'asc')` | `->oldest()` |
|
||||
| `->first()->name` | `->value('name')` |
|
||||
|
||||
## Use Laravel String & Array Helpers
|
||||
|
||||
Laravel provides `Str`, `Arr`, `Number`, and `Uri` helper classes that are more readable, chainable, and UTF-8 safe than raw PHP functions. Always prefer them.
|
||||
|
||||
Strings — use `Str` and fluent `Str::of()` over raw PHP:
|
||||
```php
|
||||
// Incorrect
|
||||
$slug = strtolower(str_replace(' ', '-', $title));
|
||||
$short = substr($text, 0, 100) . '...';
|
||||
$class = substr(strrchr('App\Models\User', '\\'), 1);
|
||||
|
||||
// Correct
|
||||
$slug = Str::slug($title);
|
||||
$short = Str::limit($text, 100);
|
||||
$class = class_basename('App\Models\User');
|
||||
```
|
||||
|
||||
Fluent strings — chain operations for complex transformations:
|
||||
```php
|
||||
// Incorrect
|
||||
$result = strtolower(trim(str_replace('_', '-', $input)));
|
||||
|
||||
// Correct
|
||||
$result = Str::of($input)->trim()->replace('_', '-')->lower();
|
||||
```
|
||||
|
||||
Key `Str` methods to prefer: `Str::slug()`, `Str::limit()`, `Str::contains()`, `Str::before()`, `Str::after()`, `Str::between()`, `Str::camel()`, `Str::snake()`, `Str::kebab()`, `Str::headline()`, `Str::squish()`, `Str::mask()`, `Str::uuid()`, `Str::ulid()`, `Str::random()`, `Str::is()`.
|
||||
|
||||
Arrays — use `Arr` over raw PHP:
|
||||
```php
|
||||
// Incorrect
|
||||
$name = isset($array['user']['name']) ? $array['user']['name'] : 'default';
|
||||
|
||||
// Correct
|
||||
$name = Arr::get($array, 'user.name', 'default');
|
||||
```
|
||||
|
||||
Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`.
|
||||
|
||||
Numbers — use `Number` for display formatting:
|
||||
```php
|
||||
Number::format(1000000); // "1,000,000"
|
||||
Number::currency(1500, 'USD'); // "$1,500.00"
|
||||
Number::abbreviate(1000000); // "1M"
|
||||
Number::fileSize(1024 * 1024); // "1 MB"
|
||||
Number::percentage(75.5); // "75.5%"
|
||||
```
|
||||
|
||||
URIs — use `Uri` for URL manipulation:
|
||||
```php
|
||||
$uri = Uri::of('https://example.com/search')
|
||||
->withQuery(['q' => 'laravel', 'page' => 1]);
|
||||
```
|
||||
|
||||
Use `$request->string('name')` to get a fluent `Stringable` directly from request input for immediate chaining.
|
||||
|
||||
Use `search-docs` for the full list of available methods — these helpers are extensive.
|
||||
|
||||
## No Inline JS/CSS in Blade
|
||||
|
||||
Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
let article = `{{ json_encode($article) }}`;
|
||||
```
|
||||
|
||||
Correct:
|
||||
```blade
|
||||
<button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}</button>
|
||||
```
|
||||
|
||||
Pass data to JS via data attributes or use a dedicated PHP-to-JS package.
|
||||
|
||||
## No Unnecessary Comments
|
||||
|
||||
Code should be readable on its own. Use descriptive method and variable names instead of comments. The only exception is config files, where descriptive comments are expected.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
// Check if there are any joins
|
||||
if (count((array) $builder->getQuery()->joins) > 0)
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
if ($this->hasJoins())
|
||||
```
|
||||
75
.github/skills/laravel-best-practices/rules/validation.md
vendored
Normal file
75
.github/skills/laravel-best-practices/rules/validation.md
vendored
Normal file
|
|
@ -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.');
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
65
.github/skills/testing-best-practices/SKILL.md
vendored
Normal file
65
.github/skills/testing-best-practices/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
name: testing-best-practices
|
||||
description: "Laravel test design and review. Use when selecting coverage, naming or structuring tests, choosing assertions or test data, isolating dependencies, testing HTTP or security boundaries, improving suite performance, or reviewing test value. Use framework guidance or search-docs for Pest and PHPUnit syntax."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Testing Best Practices
|
||||
|
||||
This skill provides rules for designing Laravel tests. Each rule file explains what to do and why. Use `search-docs` for Laravel and Pest API syntax.
|
||||
This project uses Pest. Follow the corresponding guidance in each rule.
|
||||
|
||||
## Consistency First
|
||||
|
||||
Read nearby tests before you choose syntax and organization.
|
||||
|
||||
A pattern repeated throughout the project is a convention, and project conventions take precedence over this skill. Follow them and give new tests the same structure.
|
||||
|
||||
These rules govern the tests you write now. An existing test that follows a project convention is not defective merely because it conflicts with this skill. Do not delete or rewrite it. If the convention has drawbacks, explain them and let the user decide.
|
||||
|
||||
Use the project convention for each item that follows:
|
||||
|
||||
- the use of `it()` or `test()`
|
||||
- the construction of a factory
|
||||
- the setup of the authentication
|
||||
- the layout of the files
|
||||
|
||||
## What to Test
|
||||
|
||||
Read this section before you write a test.
|
||||
|
||||
- Test observable behavior and application contracts. A test must pass after an implementation change if the behavior stays the same.
|
||||
- Cover every changed decision and each applicable high-value failure mode. A decision is a branch, a validation, a calculation, or an authorization.
|
||||
- Exercise declarations through behavior instead of repeating their text.
|
||||
- Leave framework behavior to framework tests. Testing project configuration is not testing the framework. A constrained relationship, cast, scope, or validation rule belongs to this project.
|
||||
- Keep every test that can detect a distinct defect. When two tests detect the same defect, trim the higher-layer test to one case and report the duplication. Do not delete an existing test.
|
||||
- Write a feature test first. Write a unit test only for logic that does not use the framework.
|
||||
- Write a feature test for every behavior reachable through a request. Real-browser tests require `pestphp/pest-plugin-browser` and a browser download, neither of which this project installs. Mention the package only if the user asks for a real-browser test.
|
||||
- Judge an architecture test by the convention it protects, not by the rules above. An `arch()` test declares a rule for an entire directory, such as the parent class of every model, the classes that may use an enum, or the methods every factory declares. It intentionally checks declarations and fails when a new file breaks the convention.
|
||||
- Use the test tools that the project installs. Add a new test dependency, plugin, or browser only after the user asks for it.
|
||||
|
||||
## How to Apply
|
||||
|
||||
1. Read the code under test. Read the tests in the same directory. Identify every decision in the code.
|
||||
2. Select every applicable branch in the rule index. Read every selected rule file.
|
||||
3. Report each defect in the code before you write a test. Examples are a method with no body, a policy that no action calls, and a write action with no validation. Test the actual behavior. Report the defect to the user.
|
||||
4. Write the tests. Run the smallest set of tests that covers the change. The tests must pass.
|
||||
5. Check every applicable item in `rules/review.md` and every selected rule file. Resolve every mismatch before completion.
|
||||
|
||||
## Rule Index
|
||||
|
||||
Most changes need more than one rule file.
|
||||
|
||||
| Subject | Rule file |
|
||||
| --- | --- |
|
||||
| A feature of the test framework that can already do the work | [`rules/finding-features.md`](rules/finding-features.md) |
|
||||
| The layout of the files, the names of the tests, and the groups | [`rules/naming.md`](rules/naming.md) |
|
||||
| Arrange-act-assert, and the correct assertion for each subject | [`rules/assertions.md`](rules/assertions.md) |
|
||||
| The coverage of an endpoint, the authentication, the authorization, the isolation of a tenant, the validation, and the tests in a browser | [`rules/endpoint-tests.md`](rules/endpoint-tests.md) |
|
||||
| The factories, the owner of the test data, and the repeated input values | [`rules/test-data.md`](rules/test-data.md) |
|
||||
| The fakes, the mocks, the outbound HTTP, the time, the randomness, and the database | [`rules/isolation.md`](rules/isolation.md) |
|
||||
| The escaping, the injection, the access across tenants, and the checks of privilege | [`rules/security.md`](rules/security.md) |
|
||||
| The settings of the environment and of the CI for a slow suite | [`rules/performance.md`](rules/performance.md) |
|
||||
| The review of a test or of a suite | [`rules/review.md`](rules/review.md) |
|
||||
60
.github/skills/testing-best-practices/rules/assertions.md
vendored
Normal file
60
.github/skills/testing-best-practices/rules/assertions.md
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Assertions
|
||||
|
||||
## Arrange, Act, Assert
|
||||
|
||||
Write each test in three parts: setup, one action, and assertions. Put one blank line between them so readers can identify each part without comments.
|
||||
|
||||
Keep each test self-contained. Do not use values created by another test.
|
||||
|
||||
## How to Find the Correct Assertion
|
||||
|
||||
First identify the subject of the check, then find an assertion designed for it. A subject-specific assertion identifies the incorrect value when the test fails.
|
||||
|
||||
1. Search Laravel's assertions for framework subjects such as responses, the database, sessions, models, queues, events, mail, and notifications.
|
||||
2. Fetch `https://pestphp.com/docs/expectations.md` for the expectations of Pest for a plain value, a type, a format, or a shape.
|
||||
3. Build the check by hand only if no assertion exists for the subject.
|
||||
4. Confirm the name in the documentation before you use it. Do not write an assertion that you did not confirm.
|
||||
|
||||
Use the assertion in this table for each subject.
|
||||
|
||||
| Subject | Assertion to use |
|
||||
| --- | --- |
|
||||
| A return value, the state of an object, or a transformation of a value | an `expect()` chain |
|
||||
| An HTTP status, JSON, a session, or Inertia | a Laravel response assertion |
|
||||
| The state in the database | a Laravel database assertion |
|
||||
| The existence of a model | `assertModelExists($model)` rather than `assertDatabaseHas('users', ['id' => $user->id])` |
|
||||
|
||||
Use a PHPUnit assertion only if no Pest expectation and no Laravel assertion exists for the subject.
|
||||
|
||||
Assert each fact once. Do not assert a 200 status before `assertSee`, because `assertSee` already shows that the page rendered.
|
||||
|
||||
## The Assertion with a Name for a Response
|
||||
|
||||
Use a named response assertion, such as `assertNotFound()`, rather than `assertStatus(404)`. A failure then identifies the broken contract. Laravel provides named assertions for commonly tested status codes.
|
||||
|
||||
Keep one `expect()` chain on one subject. Start a new chain when the subject changes, or when the chain is difficult to read.
|
||||
|
||||
## Assert a Known Value
|
||||
|
||||
Write the expected value in the test, or calculate the expected value by a different method. Do not calculate the expected value with the logic of the implementation, because the test then passes when that logic is wrong.
|
||||
|
||||
```php
|
||||
// The test calculates the value with the logic of the implementation.
|
||||
$expected = now()->subHours(24)->floorSeconds(30)->toJson();
|
||||
expect($from)->toBe($expected);
|
||||
|
||||
// The test sets a fixed input and asserts a known value.
|
||||
travelTo('2025-01-01 00:00:00');
|
||||
expect($from)->toBe('2024-12-31T00:00:00.000000Z');
|
||||
```
|
||||
|
||||
## Assert the Complete Result
|
||||
|
||||
A status code is not the complete result of a write operation. Assert each of the following if the operation changes it:
|
||||
|
||||
- the response or the return value
|
||||
- the state in the database
|
||||
- the jobs and the events that the operation dispatches
|
||||
- the notifications and the mail that the operation sends
|
||||
|
||||
On the failure path, assert that the operation makes none of these changes. A test that asserts only `assertOk()` passes even when the application saves no record.
|
||||
48
.github/skills/testing-best-practices/rules/endpoint-tests.md
vendored
Normal file
48
.github/skills/testing-best-practices/rules/endpoint-tests.md
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Endpoint Tests
|
||||
|
||||
## How to Write the Test
|
||||
|
||||
Fetch `https://laravel.com/framework/docs/http-tests` for the request helpers, the authentication helpers, and the response assertions. Confirm the name before you use it, and do not guess an assertion.
|
||||
|
||||
Choose an assertion based on the subject of the check: the status, a header, a redirect, the JSON body, the session, a validation error, or the view. Laravel provides a named assertion for each subject that identifies the incorrect value.
|
||||
|
||||
## The Coverage of an Endpoint
|
||||
|
||||
Write a test for each applicable case:
|
||||
|
||||
- The request has missing or invalid authentication.
|
||||
- The request comes from a different tenant, team, or organization.
|
||||
- The user has an insufficient role or permission.
|
||||
- The request does not satisfy a route or scope constraint.
|
||||
- The request fails the validation.
|
||||
- The request is valid. Assert both the response and the persisted state.
|
||||
|
||||
Assert the application's actual behavior rather than a generic status code. An API returns `401` for a missing or invalid token, while a browser endpoint redirects to the sign-in route.
|
||||
|
||||
## The Isolation of a Tenant
|
||||
|
||||
Assert the status code returned for a cross-tenant request. Use `404` rather than `403` when one tenant must not learn that another tenant's record exists, because `403` confirms its existence.
|
||||
|
||||
## Test Authorization at the Policy Level
|
||||
|
||||
An HTTP test shows that the endpoint performs authorization. It cannot identify which mechanism refused the request because middleware, a policy, and a call to `abort()` can all return `403`.
|
||||
|
||||
- Assert the complete matrix of the permissions against the policy or the gate. A failure then names the rule that is not correct.
|
||||
- Write one HTTP test for one refused role, which shows that the endpoint calls the authorization.
|
||||
- Use the helper of the project that asserts the ability and the arguments of the gate, if such a helper exists.
|
||||
|
||||
## The Validation
|
||||
|
||||
- Write one test for each validation rule when each failure represents a separate contract.
|
||||
- Write one test with an empty payload to assert several required fields together.
|
||||
- Give the status code in the name of a test for an API.
|
||||
- Assert the text of the message that the user gets. A message that is present but wrong is a defect.
|
||||
- Use a dataset for input values that need the same setup and the same assertions.
|
||||
|
||||
Send an input value that is not valid through the application, and assert the error. Do not assert that an array of rules contains a string, because that assertion tests the declaration and not the behavior. Use such an assertion only for a rule that no request can reach, and write the reason in the test.
|
||||
|
||||
### Which Layer Owns Which Case
|
||||
|
||||
The rule-class test owns the matrix of values that pass and fail. The endpoint test proves that the endpoint applies the rule and that the user receives the message.
|
||||
|
||||
When both tests contain the matrix, move it to the rule-class test and retain one case in the endpoint test. Never remove the last case, because the rule-class test still passes if the request omits the rule. The same division applies to policies, scopes, and other classes called by a request.
|
||||
36
.github/skills/testing-best-practices/rules/finding-features.md
vendored
Normal file
36
.github/skills/testing-best-practices/rules/finding-features.md
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# How to Find Test Framework Features
|
||||
|
||||
Pest adds features faster than this skill can list them. Find an existing feature before implementing the behavior by hand.
|
||||
|
||||
- Give `search-docs` the capability you need rather than the name of a function you remember. It returns features available in the installed version.
|
||||
- Fetch `https://pestphp.com/llms.txt` for the complete feature list and additions in each release.
|
||||
- If a search returns no results, tell the user that the installed version does not provide the feature. Do not write an API that you have not confirmed.
|
||||
|
||||
Search for a feature in this table before you write the code by hand.
|
||||
|
||||
| Work that you need | Term to search for |
|
||||
| --- | --- |
|
||||
| Run one test with many input values | datasets, bound datasets |
|
||||
| Assert over many values or over a collection | higher-order expectations |
|
||||
| Remove the same setup from each test in a file | hooks, higher-order tests |
|
||||
| Apply a convention to the complete codebase | architecture testing |
|
||||
| Measure if the suite finds a defect | mutation testing |
|
||||
| Find code with no types | type coverage |
|
||||
| Reduce the time of a slow suite | parallel, profiling |
|
||||
| Run one test while you debug | filtering, `--bail`, `--dirty` |
|
||||
|
||||
## The Assertions of Laravel
|
||||
|
||||
Laravel provides assertions for each part of the framework. Fetch `https://laravel.com/framework/docs/testing` for the complete list, and search for an assertion before building a check by hand. Examples include `assertDatabaseHas()`, `assertModelExists()`, `assertSoftDeleted()`, response assertions such as `assertRedirectToRoute()` and `assertJsonPath()`, and fake assertions such as `Queue::assertPushed()` and `Notification::assertSentTo()`.
|
||||
|
||||
A hand-built check fails with `false is not true`, which identifies nothing. A framework assertion names the incorrect table, value, or response, so the failure indicates what to fix.
|
||||
|
||||
```php
|
||||
// The failure says that false is not true.
|
||||
// Instead of this
|
||||
expect(User::where('email', 'taylor@laravel.com')->exists())->toBeTrue();
|
||||
|
||||
// Use this
|
||||
// The failure names the table and the attributes that it did not find.
|
||||
$this->assertDatabaseHas('users', ['email' => 'taylor@laravel.com']);
|
||||
```
|
||||
52
.github/skills/testing-best-practices/rules/isolation.md
vendored
Normal file
52
.github/skills/testing-best-practices/rules/isolation.md
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Fakes, Mocks, and Determinism
|
||||
|
||||
Tests that depend on actual time, randomness, sleeping, or network calls can fail for reasons unrelated to the code under test. Control all four.
|
||||
|
||||
## How to Isolate a Dependency
|
||||
|
||||
Fetch `https://laravel.com/framework/docs/mocking` for Laravel's fakes, facade doubles, and fake assertions. Confirm each name before using it.
|
||||
|
||||
Identify the dependency, then choose the first applicable option. A framework fake preserves the real code path, while a mock replaces the dependency.
|
||||
|
||||
1. Use framework fakes for facades such as events, queues, mail, notifications, storage, the HTTP client, time, and sleep.
|
||||
2. Use the fake implementation of the project for a service of the project, if such a fake exists.
|
||||
3. Use a mock for a container-resolved contract only when the real implementation leaves the process or is nondeterministic.
|
||||
4. Use the real implementation for everything else, including the database.
|
||||
|
||||
## The Fakes
|
||||
|
||||
- Create each fake inside the test that needs it. Do not create fakes in a file-level `beforeEach()`.
|
||||
- Pass class names to `Event::fake()` and `Queue::fake()` when you know which classes the code dispatches. A fake without class names can hide an unexpected dispatch.
|
||||
- Use a fake without class names only when the test asserts the complete result, including a call to `assertNothingPushed()`.
|
||||
- Write one assertion for each fake. The assertion states that the code dispatches the item, or that the code does not dispatch the item.
|
||||
- Assert the data of a job or of an event if that data is part of the behavior.
|
||||
- Use `Exceptions::fake()` to assert that the application reports the correct exception. Do not use `withoutExceptionHandling()`, because it changes the response under test.
|
||||
|
||||
Create prerequisite factory records before calling `Event::fake()`. Factories use model events, such as a `creating` hook that generates a UUID, and a fake without class names suppresses those events and can produce an invalid model. Call the fake first only when a factory event is under test, and pass that event's class name.
|
||||
|
||||
## The Mocks
|
||||
|
||||
Use `shouldReceive()` before the action to declare an expectation. Use `shouldHaveReceived()` after the action for a spy. Use `Mockery::on()` or `withArgs()` if an equality check cannot state the expected argument, such as a check of one field of a value object.
|
||||
|
||||
Import the mock function before you use it: `use function Pest\Laravel\mock;`.
|
||||
|
||||
## The Outbound HTTP
|
||||
|
||||
Call `Http::preventStrayRequests()`. Any request without a matching fake then fails without reaching the network.
|
||||
|
||||
Fake the exact endpoint used by each test. Do not call `Http::fake()` without an endpoint because it accepts unexpected requests and can hide defects.
|
||||
|
||||
## The Time and the Randomness
|
||||
|
||||
- Freeze the time or move the time in each test that depends on a date, a period, or a timestamp.
|
||||
- Use the framework helpers `freezeTime()`, `travelTo()`, `travel()`, and `travelBack()`. Do not call `Carbon::setTestNow()`.
|
||||
- Use `Str::createRandomStringsUsing()` to fix a generated string, if the test asserts an identifier or a slug.
|
||||
- Use `Sleep::fake()` instead of a real sleep, and assert the sleeps that the code requests.
|
||||
- Restore the time and the randomness after each test, if the suite does not restore them for every test.
|
||||
|
||||
## The Database
|
||||
|
||||
- Run the real query against the real records in the test database. Do not mock the query builder, because the test then asserts the mock.
|
||||
- Assert the exact keys of `toArray()` if the shape of the serialized model is a contract. The test then fails when the model exposes a new attribute.
|
||||
- Test application behavior caused by the schema, such as deleting dependent records through a cascade. Do not test the database engine's cascade implementation.
|
||||
- Use `LazilyRefreshDatabase` instead of `RefreshDatabase`. A test that does not use the database then does not run the migrations.
|
||||
45
.github/skills/testing-best-practices/rules/naming.md
vendored
Normal file
45
.github/skills/testing-best-practices/rules/naming.md
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Naming and Structure
|
||||
|
||||
## File Layout
|
||||
|
||||
- Name each test file `{ClassName}Test.php`.
|
||||
- Place each test file at the same relative path as the class under test. The class `app/Actions/DeleteTeam.php` gets the test `tests/Unit/Actions/DeleteTeamTest.php`.
|
||||
- Follow the project's convention for fixture files. If none exists, put fixtures in `tests/Fixtures/` and load them by path.
|
||||
- Move large literal values out of the test body and into fixture files.
|
||||
|
||||
## The Test Function
|
||||
|
||||
Use the test function used by other files in the same directory. If no neighboring test files exist:
|
||||
|
||||
- Use `it()` for the behavior of the code, and write the name as a verb phrase.
|
||||
- Use `test()` for a declarative fact, such as a grant in a policy, the labels of an enum, or the shape of a serialized model.
|
||||
|
||||
Use one Pest declaration style in each file. Use either `it()` or `test()` consistently.
|
||||
|
||||
## The Names of the Tests
|
||||
|
||||
The name of a test is a specification. State the user-visible result and the condition that causes it.
|
||||
|
||||
- Name the behavior, and not the method under test. The file name already gives the class.
|
||||
- Give the exact status code in the name of a test for an API error.
|
||||
- Do not write `Given`, `When`, or `Then` in the name.
|
||||
|
||||
```php
|
||||
it('returns 401 when no token is provided', function () { ... });
|
||||
it('does not include deployments from deleted environments', function () { ... });
|
||||
it('falls back to the default region when none is configured', function () { ... });
|
||||
```
|
||||
|
||||
Use a verb that describes a result, such as `returns`, `renders`, `creates`, `dispatches`, `rejects`, `forbids`, `falls back`, or `does not`.
|
||||
|
||||
Do not write `it('works correctly')` or `it('returns data')`, because neither specifies a meaningful result. Do not write `it('handleMethod creates record')`, because it names a method rather than behavior.
|
||||
|
||||
## Grouping
|
||||
|
||||
Use `describe()` if one file covers separate actions in a lifecycle. An example is a controller with the actions `index`, `show`, `store`, `update`, and `destroy`.
|
||||
|
||||
Do not use `describe()` in these cases:
|
||||
|
||||
- The file covers one action or one flow.
|
||||
- The tests are different only in the input value. Use a dataset instead.
|
||||
- The group adds a level but does not make the file easier to read.
|
||||
46
.github/skills/testing-best-practices/rules/performance.md
vendored
Normal file
46
.github/skills/testing-best-practices/rules/performance.md
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Test Suite Performance
|
||||
|
||||
These settings apply to the project and CI, not to individual tests. Read `rules/isolation.md` for choices within a test.
|
||||
|
||||
Fetch `https://pestphp.com/docs/optimizing-tests` for Pest options that make test runs faster.
|
||||
Verify each flag in the documentation before adding it to CI.
|
||||
|
||||
Measure before changing a setting. Find the slow test first, and apply a project-wide setting only after identifying the costly work.
|
||||
|
||||
## The Environment
|
||||
|
||||
- Set `BCRYPT_ROUNDS=4` in `.env.testing` or in `phpunit.xml`. The default value is 12, and the hash then takes most of the time of each test that signs a user in.
|
||||
- Disable XDebug. Disable pcov also, unless the run needs the coverage.
|
||||
- Disable packages that perform work on every request in the test environment. Examples are Pulse, Telescope, and Nightwatch.
|
||||
- Use the `WithCachedConfig` and `WithCachedRoutes` traits, so the run does not parse the configuration and the routes for every test.
|
||||
- Call `withoutVite()`, or `withoutMix()`, so the framework does not resolve a built asset.
|
||||
|
||||
## The Global Fakes
|
||||
|
||||
Put these three calls in the base `Pest.php` of the project:
|
||||
|
||||
- `Http::preventStrayRequests()`, because one request that reaches the network can slow the suite. This catches requests made through Laravel's HTTP client. Check direct Guzzle and cURL usage separately.
|
||||
- `Sleep::fake(syncWithCarbon: true)`, so a retry and a backoff do not sleep.
|
||||
- `Exceptions::fake()`, so the suite does not report an exception to an external service.
|
||||
|
||||
## How to Run the Suite in Parallel
|
||||
|
||||
Run `vendor/bin/pest --parallel` to spread tests across the machine's CPU cores. Add `--processes=N` if the default count is unsuitable for the machine or CI.
|
||||
|
||||
A parallel run gives each process a separate database. Tests must meet these conditions; a test that fails only in parallel breaks one of them:
|
||||
|
||||
- The test creates each record that it reads. It does not read a record that another test creates.
|
||||
- The test does not depend on the order of the run.
|
||||
- The test does not share a file, a cache key, or a queue with another test. Give each process a separate name for such a resource.
|
||||
|
||||
## How to Find a Slow Test
|
||||
|
||||
Run `vendor/bin/pest --profile` to list the slowest tests. Start with the ten slowest tests, because the same cause often applies to the complete suite.
|
||||
|
||||
If the cause of a slow test is unclear, add an event listener or temporary log entry to identify its work.
|
||||
|
||||
## Common Errors
|
||||
|
||||
- The run loads XDebug for a test that does not need it.
|
||||
- `BCRYPT_ROUNDS` keeps the default value, because the project has no `.env.testing`.
|
||||
- The code under test calls the real `sleep()`, and `Sleep::fake()` then does not help.
|
||||
53
.github/skills/testing-best-practices/rules/review.md
vendored
Normal file
53
.github/skills/testing-best-practices/rules/review.md
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Reviewing Tests
|
||||
|
||||
Check every item in this file. A passing test may still provide no value. For each test, identify the defect it would catch.
|
||||
|
||||
Report each finding. Do not delete or rewrite a test without the user's approval. When an issue appears throughout the suite as a convention, report the pattern once rather than every affected file.
|
||||
|
||||
## The Value of the Test
|
||||
|
||||
Apply this section to behavioral tests. An architecture test states a convention for a directory, so these items do not apply to it.
|
||||
|
||||
- [ ] Each test covers observable behavior or an application contract, and passes after a change to the implementation that keeps the behavior.
|
||||
- [ ] Each tested declaration is exercised through behavior, and no test asserts the behavior of the framework. A test of what this project configures, such as a relation with a constraint, a cast, or a scope, belongs to this project.
|
||||
- [ ] Each test detects a distinct defect that no other test covers. A duplicate shrinks at the higher layer to the one case that proves the wiring.
|
||||
- [ ] Every changed decision and each applicable high-value failure mode has coverage.
|
||||
|
||||
## Names and Structure
|
||||
|
||||
- [ ] Each file has the name `{ClassName}Test.php` and the relative path of the class under test.
|
||||
- [ ] Each name states a result, the condition that causes it, and the status code for an API error.
|
||||
- [ ] Each file uses one declaration style consistently, and each `describe()` group holds separate behavior.
|
||||
|
||||
## The Coverage
|
||||
|
||||
- [ ] HTTP tests cover authentication, authorization, role, scope, and validation when applicable.
|
||||
- [ ] A request for a record of a different tenant gets a status code that does not confirm that the record exists.
|
||||
- [ ] The complete permission matrix belongs in policy tests, not controller tests.
|
||||
- [ ] Each validation rule has one test that asserts the user-visible message. When a unit test owns a matrix, reduce duplicate higher-level coverage to one case rather than deleting it.
|
||||
- [ ] Rendered user input and each dynamic part of a query have a security test.
|
||||
|
||||
## The Data and the Determinism
|
||||
|
||||
- [ ] Each test creates its mutable records directly or through a helper that it calls, and every created record arranges the behavior or supports an assertion.
|
||||
- [ ] Each `beforeEach()` holds configuration only.
|
||||
- [ ] Each factory state and each relationship gives the meaning of the data.
|
||||
- [ ] Each call to `make()` is in a test that does not need the database.
|
||||
- [ ] Time, randomness, sleep, and outbound HTTP are controlled.
|
||||
- [ ] Each test passes alone, and passes in the complete suite in any order.
|
||||
|
||||
## The Assertions
|
||||
|
||||
- [ ] Each expected value is a known value, and the test does not calculate the value with the logic of the implementation.
|
||||
- [ ] Each test of a write operation asserts the response, the state in the database, and the side effects.
|
||||
- [ ] Each fake has one assertion, and gives the class names unless the test asserts the complete result.
|
||||
- [ ] Each `expect()` chain stays on one subject.
|
||||
|
||||
## The Defects to Report
|
||||
|
||||
A review can find defects in the code rather than the tests. Report each defect below, and do not write a test that codifies it as correct behavior.
|
||||
|
||||
- [ ] A method with no body.
|
||||
- [ ] A policy that exists, but that no action calls.
|
||||
- [ ] A write action with no validation.
|
||||
- [ ] A status code or a response shape that is different from the shape of a similar endpoint.
|
||||
27
.github/skills/testing-best-practices/rules/security.md
vendored
Normal file
27
.github/skills/testing-best-practices/rules/security.md
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Security Tests
|
||||
|
||||
Test each security boundary where user input affects authorization, rendered output, or query construction. A defect at such a boundary can be difficult to detect because the feature may continue to work.
|
||||
|
||||
Write a test for each of these cases:
|
||||
|
||||
- **Cross-tenant access.** Request a record of a different tenant, team, or organization. Read `rules/endpoint-tests.md` for why the response should be `404` rather than `403`.
|
||||
- **Each unprivileged role.** Use a dataset over the roles that the endpoint must refuse.
|
||||
- **Escaping user-provided content.** Test escaping in HTML and mail. Include names and every free-text field a template renders. Assert that dangerous characters are escaped and the raw value is absent. Do not assert an exact entity for a quote, because Markdown and mail CSS inliners may decode it.
|
||||
- **Injection into dynamic query components.** Examples include sort columns, filter fields, and sort directions.
|
||||
- **An unexpected key** in a payload or configuration array. A merge that accepts every key can set an attribute the user must not control.
|
||||
|
||||
```php
|
||||
it('escapes dangerous content in the notification', function () {
|
||||
$organization = Organization::factory()->make([
|
||||
'name' => "O'Reilly <script>alert('xss')</script>",
|
||||
]);
|
||||
|
||||
$content = (new QuotaApproaching($organization, 80))->toMail()->render();
|
||||
|
||||
expect($content)
|
||||
->toContain('<script>')
|
||||
->not->toContain("<script>alert('xss')</script>");
|
||||
});
|
||||
```
|
||||
|
||||
Laravel provides defenses against mass assignment, unauthorized access, and unescaped output. Test that the application applies the appropriate defense to each attribute, route, and template.
|
||||
56
.github/skills/testing-best-practices/rules/test-data.md
vendored
Normal file
56
.github/skills/testing-best-practices/rules/test-data.md
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Factories and Test Data
|
||||
|
||||
## Each Test Makes Its Own Data
|
||||
|
||||
Create mutable records inside the test that uses them. This keeps setup visible and lets each test select its factory state.
|
||||
|
||||
Use `beforeEach()` only for configuration that applies to every test in the file. Do not create records in it.
|
||||
|
||||
## Record Construction
|
||||
|
||||
- Use `create()` if the test needs the record in the database.
|
||||
- Use `make()` only if the test does not need the database. Examples include rendering a notification and testing a value object's behavior.
|
||||
- Use a named factory state instead of a raw attribute. `User::factory()->unverified()->create()` gives the state meaning; `create(['email_verified_at' => null])` gives only its value.
|
||||
- Use `for()` or the relationship helper of the project to declare the owner of a record.
|
||||
- Use `recycle()` if several records must share one parent record.
|
||||
- Use `sequence()` if several records need different attributes.
|
||||
|
||||
```php
|
||||
$organization = Organization::factory()->onPlan(BillingPlan::PRO)->create();
|
||||
|
||||
$environment = Environment::factory()->recycle($organization)->create();
|
||||
|
||||
$organizations = Organization::factory()
|
||||
->count(3)
|
||||
->sequence(
|
||||
['created_at' => now()->setSeconds(30)],
|
||||
['created_at' => now()->setSeconds(1)],
|
||||
)
|
||||
->create();
|
||||
```
|
||||
|
||||
Create only the records required to arrange the behavior or support an assertion.
|
||||
|
||||
## The Datasets
|
||||
|
||||
Use a dataset when the setup, test body, and assertions remain the same across input values.
|
||||
|
||||
```php
|
||||
it('forbids roles other than admin', function (Role $role) {
|
||||
actingAs(User::factory()->hasOrganization($role)->create())
|
||||
->post('/settings')
|
||||
->assertForbidden();
|
||||
})->with(collect(Role::cases())->reject(fn (Role $role) => $role === Role::ADMIN));
|
||||
```
|
||||
|
||||
Use parameterized tests for:
|
||||
|
||||
- the cases of an enum
|
||||
- the roles and the plans
|
||||
- the boundary values
|
||||
- the input values that are not valid in the same way
|
||||
- the pairs of an input value and an output value
|
||||
|
||||
Write separate tests if the cases need a different setup, a different behavior, or different assertions. One test function with a branch in the body is two tests in one function.
|
||||
|
||||
Give each dataset case a name that states the difference. A failure then identifies the case without requiring you to count positions.
|
||||
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.codex
|
||||
/.cursor/
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/fonts-manifest.dev.json
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
/coverage-report
|
||||
/coverage
|
||||
_ide_helper.php
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
143
.gitlab-ci.yml
Normal file
143
.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
stages:
|
||||
- install
|
||||
- lint
|
||||
- test
|
||||
- security
|
||||
- build
|
||||
- deploy
|
||||
|
||||
include:
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "security/composer.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "build/composer.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "build/docker.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "test/test.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "deploy/deploy.yml"
|
||||
|
||||
# * LINT --------------------------------------------------------------------
|
||||
|
||||
.php-lint-base: &php-lint-base
|
||||
image: composer:2.10
|
||||
variables:
|
||||
COMPOSER_CACHE_DIR: ".composer-cache"
|
||||
cache:
|
||||
key:
|
||||
files:
|
||||
- composer.lock
|
||||
prefix: "composer"
|
||||
policy: pull
|
||||
paths:
|
||||
- .composer-cache/
|
||||
- vendor/
|
||||
before_script:
|
||||
- apk add --no-cache icu-dev && docker-php-ext-install bcmath intl
|
||||
- composer config gitlab-token.lab.s-web.fr "${CI_DEPLOY_USER}" "${CI_DEPLOY_PASSWORD}"
|
||||
- composer install --no-interaction --optimize-autoloader --no-scripts
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "develop"
|
||||
|
||||
parallel-lint:
|
||||
<<: *php-lint-base
|
||||
stage: lint
|
||||
script:
|
||||
- vendor/bin/parallel-lint --exclude vendor --exclude storage --exclude bootstrap --exclude node_modules .
|
||||
allow_failure: false
|
||||
|
||||
pint:
|
||||
<<: *php-lint-base
|
||||
stage: lint
|
||||
script:
|
||||
- vendor/bin/pint --test
|
||||
allow_failure: false
|
||||
|
||||
phpcs:
|
||||
<<: *php-lint-base
|
||||
stage: lint
|
||||
script:
|
||||
- vendor/bin/phpcs --standard=phpcs.xml --extensions=php --ignore=vendor/*,storage/*,bootstrap/*,node_modules/* --report=junit --report-file=phpcs-report.xml
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'phpcs'
|
||||
paths:
|
||||
- phpcs-report.xml
|
||||
reports:
|
||||
junit: phpcs-report.xml
|
||||
allow_failure: false
|
||||
|
||||
phpstan:
|
||||
<<: *php-lint-base
|
||||
stage: lint
|
||||
variables:
|
||||
PHP_MEMORY_LIMIT: "512M"
|
||||
COMPOSER_CACHE_DIR: ".composer-cache"
|
||||
script:
|
||||
- php -d memory_limit=512M vendor/bin/phpstan --error-format=junit > phpstan-report.xml
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'phpstan'
|
||||
paths:
|
||||
- phpstan-report.xml
|
||||
reports:
|
||||
junit: phpstan-report.xml
|
||||
allow_failure: false
|
||||
|
||||
# * TESTS -------------------------------------------------------------------
|
||||
|
||||
tests_laravel:
|
||||
stage: test
|
||||
image: composer:2.10
|
||||
allow_failure: false
|
||||
variables:
|
||||
COMPOSER_CACHE_DIR: ".composer-cache"
|
||||
cache:
|
||||
- key:
|
||||
files:
|
||||
- composer.lock
|
||||
prefix: "composer"
|
||||
policy: pull
|
||||
paths:
|
||||
- .composer-cache/
|
||||
- vendor/
|
||||
before_script:
|
||||
- apk add --no-cache $PHPIZE_DEPS linux-headers icu-dev && docker-php-ext-install bcmath intl && pecl install pcov && docker-php-ext-enable pcov
|
||||
- composer config gitlab-token.lab.s-web.fr "${CI_DEPLOY_USER}" "${CI_DEPLOY_PASSWORD}"
|
||||
- composer install --no-interaction --prefer-dist --optimize-autoloader --no-scripts
|
||||
script:
|
||||
- cp .env.testing .env
|
||||
- php artisan key:generate
|
||||
- php artisan config:clear
|
||||
- php artisan test --coverage --min=85 --colors=never --log-junit=coverage/junit.xml --coverage-cobertura=coverage/cobertura.xml --coverage-html=coverage/html
|
||||
after_script:
|
||||
- if [ "$CI_JOB_STATUS" == "failed" ]; then cat storage/logs/laravel.log; fi
|
||||
artifacts:
|
||||
when: always
|
||||
expose_as: 'coverage'
|
||||
paths:
|
||||
- coverage/html/
|
||||
reports:
|
||||
junit: coverage/junit.xml
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage/cobertura.xml
|
||||
coverage: '/^\s*Total:\s*([\d\.]+)%/'
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "trunk" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"
|
||||
- if: $CI_COMMIT_TAG != null
|
||||
|
||||
# * BUILD -------------------------------------------------------------------
|
||||
|
||||
npm-build:
|
||||
stage: build
|
||||
script:
|
||||
- echo "No NPM build required for API"
|
||||
57
.intellipest/_pest-helper.php
Normal file
57
.intellipest/_pest-helper.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Runs the given closure after each test in the current file.
|
||||
*
|
||||
* @param-closure-this \Tests\TestCase|\PHPUnit\Framework\TestCase $closure
|
||||
*/
|
||||
function afterEach(?Closure $closure = null): \Pest\PendingCalls\AfterEachCall {}
|
||||
|
||||
/**
|
||||
* Runs the given closure before each test in the current file.
|
||||
*
|
||||
* @param-closure-this \Tests\TestCase|\PHPUnit\Framework\TestCase $closure
|
||||
*/
|
||||
function beforeEach(?Closure $closure = null): \Pest\PendingCalls\BeforeEachCall {}
|
||||
|
||||
/**
|
||||
* Adds the given closure as a test. The first argument
|
||||
* is the test description; the second argument is
|
||||
* a closure that contains the test expectations.
|
||||
*
|
||||
* @param-closure-this \Tests\TestCase|\PHPUnit\Framework\TestCase $closure
|
||||
*
|
||||
* @return ($description is string ? TestCall : HigherOrderTapProxy|TestCall)
|
||||
*/
|
||||
function test(?string $description = null, ?Closure $closure = null): \Pest\Support\HigherOrderTapProxy|\Pest\PendingCalls\TestCall {}
|
||||
|
||||
/**
|
||||
* Adds the given closure as a test. The first argument
|
||||
* is the test description; the second argument is
|
||||
* a closure that contains the test expectations.
|
||||
*
|
||||
* @param-closure-this \Tests\TestCase|\PHPUnit\Framework\TestCase $closure
|
||||
*/
|
||||
function it(string $description, ?Closure $closure = null): \Pest\PendingCalls\TestCall {}
|
||||
|
||||
}
|
||||
|
||||
namespace Tests {
|
||||
|
||||
class TestCase
|
||||
{
|
||||
use \Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Tests {
|
||||
|
||||
class TestCase
|
||||
{
|
||||
use \Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
}
|
||||
|
||||
}
|
||||
278
AGENTS.md
Normal file
278
AGENTS.md
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
<laravel-boost-guidelines>
|
||||
=== 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 running on PHP 8.5. You are an expert with the Laravel ecosystem. Always use the APIs that match the installed major version of each package — do not assume a version.
|
||||
|
||||
Before relying on a package's API, confirm its installed version:
|
||||
- PHP packages: run `composer show --direct` to list direct dependencies with versions, or `composer show <vendor/package>` for a single package.
|
||||
- JS packages: check `package.json` for the installed versions.
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available in `**/skills/**`. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
## 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)
|
||||
|
||||
- Use `search-docs` before changes that depend on Laravel ecosystem APIs, behavior, configuration, or version-specific syntax. Skip it for copy-only edits and other changes where package documentation is irrelevant. Reuse sufficient results already in context instead of searching again.
|
||||
- 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"]`.
|
||||
|
||||
## Project Rules
|
||||
|
||||
- This project contains committed, area-grouped rules in `.ai/rules` when that directory exists (settled decisions, non-obvious traps, standing constraints). Framework and package guidelines that only apply to specific paths (testing, frontend, components) also live there, under `.ai/rules/boost` — this is not just recorded decisions, it is load-bearing guidance you have not seen inline. Before you enter plan mode or create/edit any file, you MUST first: open @.ai/rules/index.md (it maps file globs to rule files), read every rule file whose globs cover the path(s) in scope, and run `grep -rin 'keyword' .ai/rules` to catch what a path match alone misses. Do not write code until you have read and are following every matching rule. If `.ai/rules` does not exist, continue without it.
|
||||
- Record durable rules with `record-rule` so the next agent or teammate inherits them instead of working them out again. Pass a `glob` (e.g. `app/Http/Controllers/**`), a short `title`, and a few-line `note`. Always use `record-rule`, never your native memory or notes tool — native memory is personal and session-scoped; only `.ai/rules` is shared with the team and persists in the repo.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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`
|
||||
- Follow existing application Enum naming conventions.
|
||||
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
|
||||
- Use array shape type definitions in PHPDoc blocks.
|
||||
|
||||
=== deployments rules ===
|
||||
|
||||
# Deployment
|
||||
|
||||
- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.
|
||||
|
||||
=== tests rules ===
|
||||
|
||||
# Test Enforcement
|
||||
|
||||
- Test every code change by adding or updating a test.
|
||||
- Run the affected tests and ensure they pass.
|
||||
- Test the changed behavior and its important failure modes, but do not add tests beyond them.
|
||||
- Read the `testing-best-practices` skill before writing tests.
|
||||
|
||||
=== 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. Create tests with `php artisan make:test --pest {name}`.
|
||||
- Do not include the test suite directory in `{name}`. Use `SomeFeatureTest`, not `Feature/SomeFeatureTest`.
|
||||
- Read the `testing-best-practices` skill for guidance on coverage, naming, structure, dependency isolation, and review.
|
||||
- Do not delete tests or test files without approval. They are part of the application.
|
||||
|
||||
## Running Tests
|
||||
|
||||
- Run the narrowest set of tests that covers the change. Pass a file path or `--filter=testName` to `php artisan test --compact`.
|
||||
- Rerun a test after each change to it.
|
||||
- Run `vendor/bin/pest` to call the test runner directly. It accepts the same file path and `--filter=testName` arguments.
|
||||
- After the feature tests pass, ask the user to run the complete suite with `php artisan test --compact`.
|
||||
|
||||
=== mrpunyapal/laravel-auditor/core rules ===
|
||||
|
||||
## Laravel Auditor
|
||||
|
||||
Laravel Auditor equips an **existing** AI coding agent with an evidence-based Laravel audit methodology. It does not scan the app by itself. Use it when the user asks to audit, review, or assess a Laravel application.
|
||||
|
||||
### Relationship to Boost
|
||||
|
||||
Laravel Auditor extends Laravel Boost. It does **not** replace Boost's general Laravel context. Continue using Boost's documentation search and general Laravel guidelines. Use Laravel Auditor when asked to audit, review, or assess an existing Laravel application.
|
||||
|
||||
### What Laravel Auditor provides
|
||||
|
||||
- A structured audit workflow: **Discover** project facts, **Scope** relevant domains, **Investigate** with evidence, **Verify** high-severity findings, and **Report** structured findings.
|
||||
- Six audit domains: security, performance, architecture, database, testing, and Laravel conventions.
|
||||
- Stable audit rules with rule IDs (e.g. `AUD-SEC-001`), severity, confidence, evidence requirements, and false-positive considerations.
|
||||
- Context tools that expose deterministic Laravel facts: project info, routes, models, migrations, database schema, dependencies, configuration, authorization, jobs/events/schedules, tests, and subsystems.
|
||||
- A finding schema: id, rule ID, title, domain, severity, confidence, status, summary, why-it-matters, evidence, affected resources, recommendation, remediation, verification notes, and optional `metadata.priority` (`p0`–`p3`).
|
||||
|
||||
### Installing
|
||||
|
||||
```bash
|
||||
composer require --dev mrpunyapal/laravel-auditor
|
||||
php artisan boost:install
|
||||
```
|
||||
|
||||
When Boost is not installed, use the package's own installer instead:
|
||||
|
||||
```bash
|
||||
php artisan auditor:install --agents=claude_code
|
||||
```
|
||||
|
||||
Useful commands: `auditor:status`, `auditor:rules` (`--applicable`), `auditor:context`, `auditor:report`, `auditor:ci`, and `auditor:mcp`.
|
||||
|
||||
`composer audit` is **on by default**. Test-case listing is **off by default**. Do not treat `composer_audit.available: false` or an empty advisory list as “no vulnerabilities” unless the check actually ran.
|
||||
|
||||
### Audit skill
|
||||
|
||||
Use the `laravel-audit` skill when asked to audit or review a Laravel application. It contains the full workflow, evidence requirements, and severity/confidence guidance. Domain skills (`laravel-audit-security`, `laravel-audit-performance`, `laravel-audit-architecture`, `laravel-audit-database`, `laravel-audit-testing`, `laravel-audit-conventions`) go deeper once the scope is chosen. Use `laravel-audit-dsa` for a bounded subsystem / data-structure / ownership audit (`auditor:context subsystems`, P0–P3 ranking).
|
||||
|
||||
### Key rules
|
||||
|
||||
- Evidence first: every meaningful finding cites concrete file paths, lines, routes, or config keys.
|
||||
- Prefer deterministic project facts over model guesses.
|
||||
- Distinguish confirmed findings from hypotheses.
|
||||
- Never claim exploitability without sufficient evidence.
|
||||
- Read-only by default: never modify application code during an audit.
|
||||
- Few high-quality findings over noisy volume.
|
||||
|
||||
=== mrpunyapal/laravel-auditor/dsa rules ===
|
||||
|
||||
## Laravel Auditor DSA
|
||||
|
||||
When asked for a DSA, data-structure, ownership, or subsystem audit, use the `laravel-audit-dsa` skill.
|
||||
|
||||
- Read-only. Do not implement fixes during the audit.
|
||||
- Inventory subsystems first (`php artisan auditor:context subsystems`).
|
||||
- One worker per ownership boundary. At most two findings per subsystem.
|
||||
- Skip when the local code is already clear.
|
||||
- Verify every finding yourself before it enters the report.
|
||||
- Rank P0–P3. Keep P3 small. Set `metadata.priority` to `p0`–`p3` and `metadata.subsystem` to the inventory id.
|
||||
- Prefer `AUD-DSA-*` rules when they fit. Otherwise use the six core domains.
|
||||
|
||||
Do not recommend a new type just to hide existing branching.
|
||||
|
||||
=== mrpunyapal/laravel-auditor/findings rules ===
|
||||
|
||||
## Laravel Auditor findings
|
||||
|
||||
Write structured findings, not a chat-only review. Match `schema/finding.schema.json`.
|
||||
|
||||
Required: `id`, `rule_id`, `title`, `domain`, `severity`, `confidence`, `summary`, `why_it_matters`.
|
||||
|
||||
Include whenever possible: `status` (`open` for new findings), `evidence`, `affected_resources`, `symbol`, `recommendation`, `remediation`, `verification_notes`, and `metadata.priority` (`p0`–`p3`).
|
||||
|
||||
Write JSON to `storage/auditor-findings.json` and render:
|
||||
|
||||
```bash
|
||||
php artisan auditor:report --findings=storage/auditor-findings.json
|
||||
php artisan auditor:ci --findings=storage/auditor-findings.json --fail-on=high
|
||||
```
|
||||
|
||||
Preview the packaged example with `php artisan auditor:report --example`.
|
||||
|
||||
=== mrpunyapal/laravel-auditor/performance rules ===
|
||||
|
||||
## Laravel Auditor performance findings
|
||||
|
||||
Performance findings follow the full pipeline: Signal → Context → Behavior → Verification → Impact → Finding. A suspicious pattern is not automatically a performance bug.
|
||||
|
||||
### Before reporting
|
||||
|
||||
1. Check what else consumes the value. `$users = User::where(...)->get(); $count = $users->count();` with the collection rendered by the view is **correct code** — do not recommend `->count()`. Only materialize-then-reduce with no other consumer is a finding.
|
||||
2. Verify semantic equivalence for this exact usage: accessors/casts, comparison strictness (Collection `where()` is loose, SQL is not), null handling, custom collection classes, primary/foreign keys for narrowed selects, model events on bulk writes.
|
||||
3. Describe impact by mechanism — "avoids transferring every matching row into PHP" — never invented multipliers like "10x faster". Cite numbers only from real runtime evidence.
|
||||
4. Add `metadata.impact` when evidence allows: `resource` (database/memory/network/IO), `mechanism` (what is wasted and what the fix avoids), `amplification` (loop counts, table growth, request frequency).
|
||||
|
||||
### Severity discipline
|
||||
|
||||
- `high`: query explosion or heavy amplification on hot paths (data-driven per-row queries, unbounded retrieval on public endpoints).
|
||||
- `medium`: clear unnecessary database work, avoidable materialization, repeated queries, rendering-path queries with real traffic.
|
||||
- `low`: small inefficiencies on modest data.
|
||||
- Correct micro-optimizations on tiny datasets are not findings.
|
||||
|
||||
### Noise floor
|
||||
|
||||
Do not report: reused collections/results, replacements you cannot show equivalent, bounded small datasets, caching without a key/invalidation story, concurrency for dependent requests, or eager loading "just in case" (over-eager loading is itself waste).
|
||||
|
||||
Use `php artisan auditor:rules --domain=performance --applicable` to list the rules (`AUD-PER-*`, plus `AUD-LW-003`/`AUD-FIL-003`/`AUD-IN-003` when those packages are installed). The `laravel-audit-performance` skill contains the full methodology and checklist.
|
||||
|
||||
</laravel-boost-guidelines>
|
||||
116
README.md
116
README.md
|
|
@ -1,93 +1,41 @@
|
|||
# Arcadia Api
|
||||
# Arcadia API
|
||||
|
||||
**Arcadia API** est une API de référence conçue selon les principes de la **Clean Architecture**, du **Domain-Driven Design (DDD)** et de l'**Architecture Hexagonale (Ports & Adapters)**.
|
||||
|
||||
---
|
||||
|
||||
## Getting started
|
||||
## 📚 Documentation pour les Développeurs
|
||||
|
||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
||||
L'ensemble de la documentation technique et des guides pratiques est organisé dans le dossier [`documentation/`](documentation/) :
|
||||
|
||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
||||
### 🏛️ Architecture & Fonctionnement
|
||||
- **[01 - Architecture Globale](documentation/Architecture/01-architecture-globale.md)** : Vue d'ensemble des couches (`Domain`, `Application`, `Presentation`, `Infra`, `Shared`), responsabilités et règles de dépendances strictes.
|
||||
- **[02 - Cycle de Vie d'une Requête (Bullet Tracer)](documentation/Architecture/02-cycle-de-vie-requete.md)** : Traçage pas-à-pas d'une requête HTTP depuis la route jusqu'à la sérialisation JSON.
|
||||
- **[03 - Exécution, Transactions & Événements](documentation/Architecture/03-execution-et-transactions.md)** : `UsecaseBuilder`, `LaravelExecutor`, exécution transactionnelle et dispatch sécurisé post-commit (`TransactionalDomainEventDispatcher`).
|
||||
- **[04 - Guide Nouveau Cas d'Usage](documentation/Architecture/04-guide-nouveau-usecase.md)** : Checklist pas-à-pas pour créer une nouvelle fonctionnalité de bout en bout.
|
||||
- **[05 - Temps, Horloge PSR-20 & Base de Données](documentation/Architecture/05-temps-et-base-de-donnees.md)** : Contrat PSR-20 `Clock`, Façade `Utc`, tests avec `FrozenClock`, standardisation `DATETIME` et dérivation `created_at` depuis UUIDv7.
|
||||
|
||||
## Add your files
|
||||
### 📐 Conventions & Standards
|
||||
- **[Conventions de Tests](documentation/Conventions/tests.md)** : Principes F.I.R.S.T., structure GWT, pyramide de tests, **Test Data Builders** et **Object Mothers**.
|
||||
- **[Gestion des Exceptions](documentation/Conventions/exceptions.md)** : Matrice de correspondance entre exceptions applicatives/domaine et codes HTTP RFC 7807 (400, 404, 409, 422, 429, 500).
|
||||
|
||||
* [Create](https://docs.gitlab.com/user/project/repository/web_editor/#create-a-file) or [upload](https://docs.gitlab.com/user/project/repository/web_editor/#upload-a-file) files
|
||||
* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
|
||||
---
|
||||
|
||||
## 🚀 Commandes Utiles
|
||||
|
||||
```bash
|
||||
# Lancer la suite de tests Pest
|
||||
composer test
|
||||
|
||||
# Lancer l'analyse statique PHPStan (Niveau 10)
|
||||
vendor/bin/phpstan analyse
|
||||
|
||||
# Vérifier le formattage du code PSR-12 (Pint)
|
||||
vendor/bin/pint --test
|
||||
|
||||
# Verifier les conventions et regles de lint (PHP Code Sniffer)
|
||||
vendor/bin/phpcs
|
||||
|
||||
# Corriger automatiquement les conventiosn (PHP Code Beautifier)
|
||||
vendor/bin/phpcbf
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin https://lab.s-web.fr/arcadia/arcadia-api.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
```
|
||||
|
||||
## Integrate with your tools
|
||||
|
||||
* [Set up project integrations](https://lab.s-web.fr/arcadia/arcadia-api/-/settings/integrations)
|
||||
|
||||
## Collaborate with your team
|
||||
|
||||
* [Invite team members and collaborators](https://docs.gitlab.com/user/project/members/)
|
||||
* [Create a new merge request](https://docs.gitlab.com/user/project/merge_requests/creating_merge_requests/)
|
||||
* [Automatically close issues from merge requests](https://docs.gitlab.com/user/project/issues/managing_issues/#closing-issues-automatically)
|
||||
* [Enable merge request approvals](https://docs.gitlab.com/user/project/merge_requests/approvals/)
|
||||
* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
|
||||
|
||||
## Test and Deploy
|
||||
|
||||
Use the built-in continuous integration in GitLab.
|
||||
|
||||
* [Get started with GitLab CI/CD](https://docs.gitlab.com/ci/quick_start/)
|
||||
* [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/user/application_security/sast/)
|
||||
* [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/topics/autodevops/requirements/)
|
||||
* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/user/clusters/agent/)
|
||||
* [Set up protected environments](https://docs.gitlab.com/ci/environments/protected_environments/)
|
||||
|
||||
***
|
||||
|
||||
# Editing this README
|
||||
|
||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
||||
|
||||
## Suggestions for a good README
|
||||
|
||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
||||
|
||||
## Name
|
||||
Choose a self-explaining name for your project.
|
||||
|
||||
## Description
|
||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
||||
|
||||
## Badges
|
||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
||||
|
||||
## Visuals
|
||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
||||
|
||||
## Installation
|
||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
||||
|
||||
## Usage
|
||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
||||
|
||||
## Support
|
||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
||||
|
||||
## Roadmap
|
||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
||||
|
||||
## Contributing
|
||||
State if you are open to contributions and what your requirements are for accepting them.
|
||||
|
||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
||||
|
||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
||||
|
||||
## Authors and acknowledgment
|
||||
Show your appreciation to those who have contributed to the project.
|
||||
|
||||
## License
|
||||
For open source projects, say how it is licensed.
|
||||
|
||||
## Project status
|
||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
||||
|
|
|
|||
18
artisan
Executable file
18
artisan
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
28
boost.json
Normal file
28
boost.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"agents": [
|
||||
"copilot",
|
||||
"antigravity"
|
||||
],
|
||||
"cloud": false,
|
||||
"guidelines": true,
|
||||
"mcp": true,
|
||||
"nightwatch": false,
|
||||
"packages": [
|
||||
"mrpunyapal/laravel-auditor"
|
||||
],
|
||||
"sail": false,
|
||||
"skills": [
|
||||
"infer-conventions",
|
||||
"laravel-best-practices",
|
||||
"testing-best-practices",
|
||||
"laravel-audit",
|
||||
"laravel-audit-architecture",
|
||||
"laravel-audit-conventions",
|
||||
"laravel-audit-database",
|
||||
"laravel-audit-dsa",
|
||||
"laravel-audit-performance",
|
||||
"laravel-audit-security",
|
||||
"laravel-audit-testing",
|
||||
"laravel-auditor-development"
|
||||
]
|
||||
}
|
||||
24
bootstrap/app.php
Normal file
24
bootstrap/app.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Env;
|
||||
|
||||
$app = Application::configure(Env::get('APP_BASE_PATH', dirname(__DIR__)))
|
||||
->withRouting(
|
||||
web: __DIR__ . '/../routes/web.php',
|
||||
api: __DIR__ . '/../routes/api.php',
|
||||
commands: __DIR__ . '/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withCommands([
|
||||
__DIR__ . '/../src/Presentation/Console/Commands',
|
||||
])
|
||||
->withMiddleware(require __DIR__ . '/middlewares.php')
|
||||
->withExceptions(require __DIR__ . '/exceptions.php')
|
||||
->create();
|
||||
|
||||
$app->useAppPath($app->basePath('src/Presentation'));
|
||||
|
||||
return $app;
|
||||
2
bootstrap/cache/.gitignore
vendored
Normal file
2
bootstrap/cache/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
35
bootstrap/exceptions.php
Normal file
35
bootstrap/exceptions.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Http\Request;
|
||||
use Presentation\Api\Exceptions\ApiExceptionDispatcher;
|
||||
use Presentation\Web\Exceptions\WebExceptionRenderer;
|
||||
use Sentry\State\HubInterface;
|
||||
|
||||
return function (Exceptions $exceptions): void {
|
||||
$exceptions->reportable(function (\Throwable $e): void {
|
||||
if (app()->bound(HubInterface::class)) {
|
||||
resolve(HubInterface::class)->captureException($e);
|
||||
}
|
||||
});
|
||||
|
||||
$exceptions->render(function (\Throwable $e, Request $request) {
|
||||
$apiDispatcher = resolve(ApiExceptionDispatcher::class);
|
||||
if ($apiResponse = $apiDispatcher->render($e, $request)) {
|
||||
return $apiResponse;
|
||||
}
|
||||
|
||||
$webRenderer = resolve(WebExceptionRenderer::class);
|
||||
if ($webResponse = $webRenderer->render($e, $request)) {
|
||||
return $webResponse;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
$exceptions->shouldRenderJsonWhen(
|
||||
fn (Request $request) => $request->is('api/*', 'api'),
|
||||
);
|
||||
};
|
||||
32
bootstrap/middlewares.php
Normal file
32
bootstrap/middlewares.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Presentation\Api\V1\Http\Middlewares\EnsureApiV1AcceptsJson;
|
||||
use Presentation\Api\V1\Http\Middlewares\SetApiV1Etag;
|
||||
use Presentation\Api\V1\Http\Middlewares\TransformApiV1Response;
|
||||
use Presentation\Api\V1\Http\Middlewares\VerifyHmacSignature;
|
||||
|
||||
return function (Middleware $middleware): void {
|
||||
$middleware->alias([
|
||||
'auth.hmac' => VerifyHmacSignature::class,
|
||||
]);
|
||||
|
||||
$middleware->web(append: [
|
||||
'throttle:web',
|
||||
]);
|
||||
|
||||
$middleware->api(append: []);
|
||||
|
||||
$middleware->group('api.v1', [
|
||||
EnsureApiV1AcceptsJson::class,
|
||||
'throttle:api.v1',
|
||||
SetApiV1Etag::class,
|
||||
TransformApiV1Response::class,
|
||||
]);
|
||||
|
||||
$middleware->preventRequestsDuringMaintenance(except: [
|
||||
'errors/*',
|
||||
]);
|
||||
};
|
||||
17
bootstrap/providers.php
Normal file
17
bootstrap/providers.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Infra\Providers\AppServiceProvider;
|
||||
use Infra\Providers\ConfigServiceProvider;
|
||||
use Infra\Providers\DomainServiceProvider;
|
||||
use Infra\Providers\EventServiceProvider;
|
||||
use Infra\Providers\SecurityServiceProvider;
|
||||
|
||||
return [
|
||||
ConfigServiceProvider::class,
|
||||
AppServiceProvider::class,
|
||||
DomainServiceProvider::class,
|
||||
EventServiceProvider::class,
|
||||
SecurityServiceProvider::class,
|
||||
];
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue