---
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(...))
```
