--- name: api-platform description: Conventions API Platform du projet Mangarr — brancher un State Processor sur une Command, un State Provider sur une Query, nommage des Resources, gestion des DTOs. Utiliser quand on crée ou modifie une Resource, un State Processor/Provider, ou un DTO API Platform. allowed-tools: Read, Grep, Glob --- # API Platform — Mangarr Tout le code API Platform vit dans `Infrastructure/ApiPlatform/` du domaine concerné. ``` Infrastructure/ApiPlatform/ Resource/ {FeatureName}Resource.php ← classe vide avec attribut #[ApiResource] State/ Processor/ {DoSomething}Processor.php ← implémente ProcessorInterface → Command Provider/ {GetSomething}StateProvider.php ← implémente ProviderInterface → Query Dto/ {Name}.php ← données entrantes ou sortantes Controller/ {Action}Controller.php ← uniquement pour cas non-standards ``` --- ## Resource Classe **vide** — elle ne contient que l'attribut `#[ApiResource]`. Aucune logique. ```php // Infrastructure/ApiPlatform/Resource/MangaResource.php namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Delete; use App\Domain\Manga\Infrastructure\ApiPlatform\Dto\MangaDetail; use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\GetMangaStateProvider; use App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor\CreateMangaProcessor; #[ApiResource( shortName: 'Manga', operations: [ new Get( uriTemplate: '/mangas/by-id/{id}', provider: GetMangaStateProvider::class, output: MangaDetail::class, ), new Post( uriTemplate: '/mangas', input: CreateMangaDto::class, processor: CreateMangaProcessor::class, ), new Delete( uriTemplate: '/mangas/{id}', provider: DeleteMangaProvider::class, // ← provider requis pour Delete processor: DeleteMangaProcessor::class, ), ] )] class MangaResource {} ``` **Règles Resource :** - `shortName` = nom du concept métier (ex: `'Manga'`, `'Chapter'`). - `uriTemplate` explicite (pas de génération automatique depuis le nom de classe). - `output` = DTO de sortie, `input` = DTO d'entrée. - `provider` et `processor` référencés par `::class`. --- ## State Processor → Command (écriture) ```php // Infrastructure/ApiPlatform/State/Processor/{DoSomething}Processor.php namespace App\Domain\{Domain}\Infrastructure\ApiPlatform\State\Processor; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\Domain\{Domain}\Application\Command\{DoSomething}; use App\Domain\{Domain}\Application\CommandHandler\{DoSomething}Handler; use App\Domain\{Domain}\Domain\Exception\{Something}NotFoundException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; readonly class {DoSomething}Processor implements ProcessorInterface { public function __construct( private {DoSomething}Handler $handler, ) {} public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void { try { $this->handler->handle(new {DoSomething}( $uriVariables['id'] ?? $data->someField, )); } catch ({Something}NotFoundException $e) { throw new NotFoundHttpException($e->getMessage()); } } } ``` **Règles Processor :** - Injecte le **Handler concret** (pas une interface, car l'Infrastructure peut dépendre de l'Application). - Traduit les Domain Exceptions en HTTP Exceptions Symfony (`NotFoundHttpException`, `UnprocessableEntityHttpException`…). - Retourne `void` pour les opérations sans corps de réponse, ou le DTO de sortie si nécessaire. --- ## State Provider → Query (lecture) ```php // Infrastructure/ApiPlatform/State/Provider/{GetSomething}StateProvider.php namespace App\Domain\{Domain}\Infrastructure\ApiPlatform\State\Provider; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\Domain\{Domain}\Application\Query\{GetSomething}; use App\Domain\{Domain}\Application\QueryHandler\{GetSomething}Handler; use App\Domain\{Domain}\Infrastructure\ApiPlatform\Dto\{Something}Detail; readonly class {GetSomething}StateProvider implements ProviderInterface { public function __construct( private {GetSomething}Handler $handler, ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): {Something}Detail { $query = new {GetSomething}($uriVariables['id']); $response = $this->handler->handle($query); return new {Something}Detail( id: $response->id, title: $response->title, // mapper Response → DTO ici ); } } ``` **Règles Provider :** - Construit la Query depuis `$uriVariables` et/ou `$context['filters']`. - Mappe le `Response` (Application) vers un **DTO** (Infrastructure) — ne jamais retourner un Response directement. - Pour les collections, retourner un tableau ou un objet `Paginator`. --- ## DTOs Les DTOs sont des classes de données spécifiques à la couche HTTP. Ils ne doivent pas contenir de logique. ```php // Infrastructure/ApiPlatform/Dto/MangaDetail.php namespace App\Domain\Manga\Infrastructure\ApiPlatform\Dto; readonly class MangaDetail { public function __construct( public string $id, public string $title, public string $slug, public ?string $imageUrl, // ... ) {} } ``` **Règles DTO :** - `readonly class`. - Uniquement des types PHP natifs. - **DTO d'entrée** : contient les champs que le client envoie (`input:`). - **DTO de sortie** : contient les champs que l'API retourne (`output:`). - Ne jamais réutiliser un `Response` Application comme DTO API Platform (couches séparées). --- ## Conventions de nommage | Fichier | Pattern de nom | Exemple | |------------------------------------|----------------------------------------|------------------------------------| | Resource (opération GET) | `{Concept}Resource.php` | `MangaResource.php` | | Resource (opération spécifique) | `{Action}{Concept}Resource.php` | `CreateMangaResource.php` | | Processor | `{DoSomething}Processor.php` | `CreateMangaProcessor.php` | | Provider (GET item) | `Get{Concept}StateProvider.php` | `GetMangaStateProvider.php` | | Provider (GET collection) | `Get{Concept}ListStateProvider.php` | `GetMangaListStateProvider.php` | | Provider (Delete, nécessite item) | `Delete{Concept}Provider.php` | `DeleteMangaProvider.php` | | DTO de sortie (item) | `{Concept}Detail.php` | `MangaDetail.php` | | DTO de sortie (liste) | `{Concept}ListItem.php` | `MangaListItem.php` | | DTO de sortie (collection) | `{Concept}Collection.php` | `MangaCollection.php` | --- ## Flux complet : une opération POST ``` HTTP POST /mangas → CreateMangaResource (#[ApiResource] avec processor:) → CreateMangaProcessor::process($data, ...) → CreateMangaFromMangadexHandler::handle(new CreateMangaFromMangadex(...)) → Domain : Manga::__construct(...) + invariants → MangaRepositoryInterface::save($manga) → MessageBus::dispatch(new MangaCreated(...)) ```