209 lines
7.6 KiB
Markdown
209 lines
7.6 KiB
Markdown
---
|
|
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(...))
|
|
```
|