feat: ajout de claude + correction des tests
All checks were successful
Build and Deploy / deploy (push) Successful in 9m36s
All checks were successful
Build and Deploy / deploy (push) Successful in 9m36s
This commit is contained in:
parent
b5a832fbbc
commit
dae215dd3d
208
.claude/skills/cqrs/SKILL.md
Normal file
208
.claude/skills/cqrs/SKILL.md
Normal file
@@ -0,0 +1,208 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user