Files
Mangarr/.claude/skills/cqrs/SKILL.md
ext.jeremy.guillot@maxicoffee.domains dae215dd3d
All checks were successful
Build and Deploy / deploy (push) Successful in 9m36s
feat: ajout de claude + correction des tests
2026-03-09 17:09:31 +01:00

209 lines
6.5 KiB
Markdown

---
name: cqrs
description: Patterns CQRS du projet Mangarr — templates Command/CommandHandler et Query/QueryHandler, enregistrement dans Symfony Messenger, séparation read/write model. Utiliser quand on crée ou modifie un use case (Command ou Query) et son handler.
allowed-tools: Read, Grep, Glob
---
# CQRS — Mangarr
## Principe
- **Command** : intention de modifier l'état. Retourne `void`.
- **Query** : lecture seule. Retourne un `Response` objet (jamais une entité Doctrine).
- Les handlers vivent dans `Application/`, jamais dans `Infrastructure/` directement.
- Les handlers sont `readonly class` et reçoivent leurs dépendances via le constructeur (autowiring).
---
## Template Command
```php
// src/Domain/{Domain}/Application/Command/{DoSomething}.php
namespace App\Domain\{Domain}\Application\Command;
readonly class {DoSomething}
{
public function __construct(
public string $someId,
public string $someValue,
// scalaires ou tableaux uniquement — pas d'objets du Domain
) {}
}
```
## Template CommandHandler
```php
// src/Domain/{Domain}/Application/CommandHandler/{DoSomething}Handler.php
namespace App\Domain\{Domain}\Application\CommandHandler;
use App\Domain\{Domain}\Application\Command\{DoSomething};
use App\Domain\{Domain}\Domain\Contract\Repository\{Name}RepositoryInterface;
use App\Domain\{Domain}\Domain\Event\{SomethingHappened};
use App\Domain\{Domain}\Domain\Model\{Aggregate};
use Ramsey\Uuid\Uuid;
use Symfony\Component\Messenger\MessageBusInterface;
readonly class {DoSomething}Handler
{
public function __construct(
private {Name}RepositoryInterface $repository,
// autres interfaces Domain uniquement
private MessageBusInterface $messageBus, // si event à dispatcher
) {}
public function handle({DoSomething} $command): void
{
// 1. Reconstruire/créer l'Aggregate via Value Objects
$aggregate = new {Aggregate}(
new {AggregateId}(Uuid::uuid4()->toString()),
// ...
);
// 2. Appeler la méthode métier (invariants dans le Domain)
$aggregate->doSomething();
// 3. Persister via l'interface repository
$this->repository->save($aggregate);
// 4. Dispatcher un Domain Event si nécessaire
$this->messageBus->dispatch(new {SomethingHappened}($aggregate->getId()->getValue()));
}
}
```
**Règles CommandHandler :**
- N'injecte que des interfaces définies dans `Domain/Contract/` (jamais une classe concrète Infrastructure).
- Exception autorisée : `MessageBusInterface` de Symfony Messenger.
- Ne retourne jamais de données (`void`).
---
## Template Query
```php
// src/Domain/{Domain}/Application/Query/{GetSomething}.php
namespace App\Domain\{Domain}\Application\Query;
readonly class {GetSomething}
{
public function __construct(
public string $id,
// critères de filtrage en scalaires
) {}
}
```
## Template QueryHandler
```php
// src/Domain/{Domain}/Application/QueryHandler/{GetSomething}Handler.php
namespace App\Domain\{Domain}\Application\QueryHandler;
use App\Domain\{Domain}\Application\Query\{GetSomething};
use App\Domain\{Domain}\Application\Response\{Something}Response;
use App\Domain\{Domain}\Domain\Contract\Repository\{Name}RepositoryInterface;
use App\Domain\{Domain}\Domain\Exception\{Something}NotFoundException;
readonly class {GetSomething}Handler
{
public function __construct(
private {Name}RepositoryInterface $repository,
) {}
public function handle({GetSomething} $query): {Something}Response
{
$aggregate = $this->repository->findById($query->id);
if (!$aggregate) {
throw new {Something}NotFoundException();
}
return new {Something}Response(
id: $aggregate->getId()->getValue(),
// mapper les Value Objects vers scalaires ici
);
}
}
```
## Template Response
```php
// src/Domain/{Domain}/Application/Response/{Something}Response.php
namespace App\Domain\{Domain}\Application\Response;
readonly class {Something}Response
{
public function __construct(
public string $id,
public string $title,
// scalaires et tableaux uniquement — jamais d'objets Domain
) {}
}
```
**Règles Response :**
- `readonly class`.
- Uniquement des types PHP natifs (`string`, `int`, `float`, `bool`, `array`, `?string`…).
- C'est le **read model** — il ne sert qu'à transporter des données vers l'Infrastructure.
---
## Enregistrement dans Symfony Messenger
### Command via bus synchrone
Les handlers sont auto-découverts par autowiring. Aucune configuration supplémentaire pour les CommandHandlers Application purs.
Pour les handlers Symfony Messenger (traitement asynchrone ou via `command.bus`), créer un wrapper dans `Infrastructure/CommandHandler/` :
```php
// src/Domain/{Domain}/Infrastructure/CommandHandler/Symfony{DoSomething}Handler.php
namespace App\Domain\{Domain}\Infrastructure\CommandHandler;
use App\Domain\{Domain}\Application\Command\{DoSomething};
use App\Domain\{Domain}\Application\CommandHandler\{DoSomething}Handler;
readonly class Symfony{DoSomething}Handler
{
public function __construct(
private {DoSomething}Handler $handler,
) {}
public function __invoke({DoSomething} $command): void
{
$this->handler->handle($command);
}
}
```
Déclarer dans `config/services.yaml` :
```yaml
App\Domain\{Domain}\Infrastructure\CommandHandler\Symfony{DoSomething}Handler:
tags:
- { name: messenger.message_handler, bus: command.bus }
```
### Buses disponibles
| Bus / Transport | Usage |
|------------------|------------------------------------|
| `command.bus` | Commands synchrones ou async |
| `events` | Domain Events (async) |
| `commands` | Messages async (ex: scraping) |
| `async` | Scheduler (tâches planifiées) |
---
## Séparation Read Model / Write Model
| Write Model | Read Model |
|--------------------------------------|-------------------------------------------|
| `Domain/Model/{Aggregate}.php` | `Application/Response/{Name}Response.php` |
| Contient les invariants métier | Contient uniquement des données aplaties |
| Manipulé par les CommandHandlers | Retourné par les QueryHandlers |
| Persisté via `RepositoryInterface` | Jamais persisté directement |
Ne jamais retourner un Aggregate depuis un QueryHandler — toujours mapper vers une Response.