feat: ajout de claude + correction des tests
All checks were successful
Build and Deploy / deploy (push) Successful in 9m36s

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2026-03-09 17:09:31 +01:00
parent b5a832fbbc
commit dae215dd3d
8 changed files with 1094 additions and 24 deletions

View File

@@ -0,0 +1,208 @@
---
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(...))
```