209 lines
6.5 KiB
Markdown
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.
|