$npx -y skills add aligundogdu/symfony-hexagonal-skill --skill symfony-cqrs-handlersSymfony CQRS command/query handlers — commands, queries, handlers, bus configuration, use cases. Triggers on: command, query, handler, CQRS, bus, use case, command handler, query handler, message bus
| 1 | # Symfony CQRS Handlers |
| 2 | |
| 3 | You are an expert in CQRS (Command Query Responsibility Segregation) within Symfony hexagonal architecture. |
| 4 | |
| 5 | ## When to Activate |
| 6 | |
| 7 | - User wants to create a command or query |
| 8 | - User needs a handler for a use case |
| 9 | - User asks about CQRS patterns or message bus configuration |
| 10 | - User mentions "use case", "action", "operation" in application context |
| 11 | |
| 12 | ## Command Pattern |
| 13 | |
| 14 | Commands represent write operations (create, update, delete). They are DTOs dispatched to the command bus. |
| 15 | |
| 16 | ### Rules |
| 17 | - `final readonly class` — immutable after construction |
| 18 | - Named as imperative verb: `RegisterUser`, `PlaceOrder`, `CancelSubscription` |
| 19 | - Contains only primitive types and value objects — no entities |
| 20 | - Handler returns `void` or a scalar identifier (string ID) |
| 21 | - One handler per command |
| 22 | |
| 23 | ### Template |
| 24 | ```php |
| 25 | namespace App\Application\{Module}\Command; |
| 26 | |
| 27 | final readonly class {ActionVerb}{Entity} |
| 28 | { |
| 29 | public function __construct( |
| 30 | public string $param1, |
| 31 | public string $param2, |
| 32 | // only primitives and simple types |
| 33 | ) { |
| 34 | } |
| 35 | } |
| 36 | ``` |
| 37 | |
| 38 | ### Handler Template |
| 39 | ```php |
| 40 | namespace App\Application\{Module}\Command; |
| 41 | |
| 42 | use App\Domain\{Module}\Port\{Repository}Interface; |
| 43 | use Symfony\Component\Messenger\Attribute\AsMessageHandler; |
| 44 | |
| 45 | #[AsMessageHandler(bus: 'command.bus')] |
| 46 | final readonly class {ActionVerb}{Entity}Handler |
| 47 | { |
| 48 | public function __construct( |
| 49 | private {Repository}Interface $repository, |
| 50 | ) { |
| 51 | } |
| 52 | |
| 53 | public function __invoke({ActionVerb}{Entity} $command): void |
| 54 | { |
| 55 | // 1. Reconstruct/create domain objects |
| 56 | // 2. Execute business logic |
| 57 | // 3. Persist via port |
| 58 | // NO side-effects here — use domain events |
| 59 | } |
| 60 | } |
| 61 | ``` |
| 62 | |
| 63 | ## Query Pattern |
| 64 | |
| 65 | Queries represent read operations. They return DTOs, never domain entities. |
| 66 | |
| 67 | ### Rules |
| 68 | - `final readonly class` — immutable |
| 69 | - Named descriptively: `GetUserById`, `ListActiveOrders`, `SearchProducts` |
| 70 | - Handler MUST return a DTO or array of DTOs |
| 71 | - Handler NEVER modifies state |
| 72 | - May use read-optimized ports (separate from write ports) |
| 73 | |
| 74 | ### Template |
| 75 | ```php |
| 76 | namespace App\Application\{Module}\Query; |
| 77 | |
| 78 | final readonly class {GetDescription} |
| 79 | { |
| 80 | public function __construct( |
| 81 | public string $identifier, |
| 82 | // filter/pagination params |
| 83 | ) { |
| 84 | } |
| 85 | } |
| 86 | ``` |
| 87 | |
| 88 | ### Handler Template |
| 89 | ```php |
| 90 | namespace App\Application\{Module}\Query; |
| 91 | |
| 92 | use App\Application\{Module}\DTO\{Entity}DTO; |
| 93 | use App\Domain\{Module}\Port\{Repository}Interface; |
| 94 | use Symfony\Component\Messenger\Attribute\AsMessageHandler; |
| 95 | |
| 96 | #[AsMessageHandler(bus: 'query.bus')] |
| 97 | final readonly class {GetDescription}Handler |
| 98 | { |
| 99 | public function __construct( |
| 100 | private {Repository}Interface $repository, |
| 101 | ) { |
| 102 | } |
| 103 | |
| 104 | public function __invoke({GetDescription} $query): ?{Entity}DTO |
| 105 | { |
| 106 | $entity = $this->repository->findById($query->identifier); |
| 107 | if ($entity === null) { |
| 108 | return null; |
| 109 | } |
| 110 | |
| 111 | return {Entity}DTO::fromEntity($entity); |
| 112 | } |
| 113 | } |
| 114 | ``` |
| 115 | |
| 116 | ## DTO Pattern |
| 117 | |
| 118 | ```php |
| 119 | namespace App\Application\{Module}\DTO; |
| 120 | |
| 121 | final readonly class {Entity}DTO |
| 122 | { |
| 123 | public function __construct( |
| 124 | public string $id, |
| 125 | public string $field1, |
| 126 | public string $field2, |
| 127 | ) { |
| 128 | } |
| 129 | |
| 130 | public static function fromEntity(/* entity */): self |
| 131 | { |
| 132 | return new self( |
| 133 | id: (string) $entity->id(), |
| 134 | field1: $entity->field1(), |
| 135 | field2: $entity->field2(), |
| 136 | ); |
| 137 | } |
| 138 | } |
| 139 | ``` |
| 140 | |
| 141 | ## References |
| 142 | |
| 143 | See `references/` for detailed guides: |
| 144 | - `command-patterns.md` — Full command examples with validation |
| 145 | - `query-patterns.md` — Query patterns with pagination and filtering |
| 146 | - `bus-configuration.md` — Messenger bus setup and middleware |