---
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.
