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

6.5 KiB

name, description, allowed-tools
name description allowed-tools
cqrs 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. 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

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

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

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

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

// 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/ :

// 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 :

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.