From acc
Audits PHP code for CQRS/Event Sourcing alignment: command/query separation, projection idempotency, event store consistency, read/write model sync.
How this skill is triggered — by the user, by Claude, or both
Slash command
/acc:check-cqrs-alignmentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Analyze PHP code for proper CQRS implementation and Event Sourcing compliance.
Analyze PHP code for proper CQRS implementation and Event Sourcing compliance.
// ANTIPATTERN: Command handler returns data (violates CQS)
final readonly class CreateOrderHandler
{
public function handle(CreateOrderCommand $command): OrderDTO // Returns data!
{
$order = Order::create($command->userId(), $command->items());
$this->orderRepo->save($order);
return OrderDTO::fromEntity($order); // Mixing write + read
}
}
// CORRECT: Command returns void or just ID
final readonly class CreateOrderHandler
{
public function handle(CreateOrderCommand $command): OrderId
{
$order = Order::create($command->userId(), $command->items());
$this->orderRepo->save($order);
return $order->id(); // Only identity, not projection
}
}
// CRITICAL: Query handler with side effects
final readonly class GetOrderHandler
{
public function handle(GetOrderQuery $query): OrderDTO
{
$order = $this->orderRepo->find($query->orderId());
$order->markAsViewed(); // Side effect in query!
$this->orderRepo->save($order); // Write in read path!
return OrderDTO::fromEntity($order);
}
}
// CORRECT: Query is pure read
final readonly class GetOrderHandler
{
public function handle(GetOrderQuery $query): OrderReadModel
{
return $this->orderReadRepo->find($query->orderId());
}
}
// ANTIPATTERN: Read side uses write model
final readonly class OrderListHandler
{
public function handle(OrderListQuery $query): array
{
// Using write-side repository for reads
$orders = $this->orderRepository->findByUser($query->userId());
return array_map(fn (Order $o) => OrderDTO::fromEntity($o), $orders);
// Hydrates full aggregate just to read!
}
}
// CORRECT: Dedicated read model
final readonly class OrderListHandler
{
public function handle(OrderListQuery $query): array
{
// Flat read from read-optimized storage
return $this->orderReadRepository->findByUser($query->userId());
}
}
// CRITICAL: Projection not idempotent — replaying events duplicates data
class OrderProjection
{
public function onOrderCreated(OrderCreated $event): void
{
$this->db->insert('order_read_model', [
'id' => $event->orderId(),
'total' => $event->total(),
]);
// If event replayed → duplicate row!
}
}
// CORRECT: Idempotent projection (upsert)
class OrderProjection
{
public function onOrderCreated(OrderCreated $event): void
{
$this->db->executeStatement(
'INSERT INTO order_read_model (id, total, updated_at)
VALUES (:id, :total, :updated_at)
ON DUPLICATE KEY UPDATE total = :total, updated_at = :updated_at',
[
'id' => $event->orderId()->toString(),
'total' => $event->total()->amount(),
'updated_at' => $event->occurredAt()->format('Y-m-d H:i:s'),
],
);
}
}
// ANTIPATTERN: Event missing essential metadata
final readonly class OrderCreated
{
public function __construct(
public OrderId $orderId,
public UserId $userId,
// Missing: version, timestamp, aggregate version
) {}
}
// CORRECT: Full event metadata
final readonly class OrderCreated implements DomainEvent
{
public function __construct(
public OrderId $orderId,
public UserId $userId,
public Money $total,
public int $aggregateVersion,
public \DateTimeImmutable $occurredAt,
public EventId $eventId,
) {}
}
// ANTIPATTERN: Single bus for commands and queries
class MessageBus
{
public function dispatch(mixed $message): mixed
{
// Cannot enforce "commands return void" vs "queries return data"
$handler = $this->handlers[get_class($message)];
return $handler->handle($message);
}
}
// CORRECT: Separate buses
interface CommandBus
{
public function dispatch(Command $command): void;
}
interface QueryBus
{
public function dispatch(Query $query): mixed;
}
// CRITICAL: No concurrency control on event append
class EventStore
{
public function append(AggregateId $id, array $events): void
{
foreach ($events as $event) {
$this->db->insert('events', [
'aggregate_id' => $id->toString(),
'payload' => serialize($event),
]);
}
// No version check — concurrent writes corrupt stream!
}
}
// CORRECT: Optimistic locking with expected version
class EventStore
{
public function append(AggregateId $id, array $events, int $expectedVersion): void
{
$currentVersion = $this->getVersion($id);
if ($currentVersion !== $expectedVersion) {
throw new ConcurrencyException(
"Expected version {$expectedVersion}, got {$currentVersion}",
);
}
// Append with version increment...
}
}
# Command returning data
Grep: "class.*CommandHandler.*\n.*function handle.*:.*(?!void|.*Id)" --glob "**/*.php"
Grep: "CommandHandler.*return.*DTO|CommandHandler.*return.*Response" --glob "**/*.php"
# Query with side effects
Grep: "->save\(|->persist\(|->flush\(" --glob "**/*QueryHandler*.php"
Grep: "->save\(|->persist\(|->flush\(" --glob "**/*ReadModel*.php"
# Read using write repository
Grep: "Repository->find|Repository->findBy" --glob "**/*QueryHandler*.php"
# Non-idempotent projection
Grep: "->insert\(" --glob "**/*Projection*.php"
# Missing event metadata
Grep: "class.*Event\b" --glob "**/Domain/**/*.php"
Grep: "occurredAt|aggregateVersion|eventId" --glob "**/Domain/**/*Event*.php"
# Single bus for both
Grep: "class.*Bus.*dispatch.*mixed" --glob "**/*.php"
| Pattern | Severity |
|---|---|
| Query modifying state | 🔴 Critical |
| Non-idempotent projection | 🔴 Critical |
| Event store without locking | 🔴 Critical |
| Command returning rich data | 🟠 Major |
| Read using write repository | 🟠 Major |
| Mixed command/query bus | 🟡 Minor |
| Event without version | 🟡 Minor |
### CQRS Alignment: [Description]
**Severity:** 🔴/🟠/🟡
**Location:** `file.php:line`
**Side:** Command/Query/Projection
**CQRS Rule Violated:**
[Which CQRS/ES principle is broken]
**Issue:**
[Description of the alignment violation]
**Code:**
```php
// Misaligned code
Fix:
// Properly separated code
npx claudepluginhub dykyi-roman/awesome-claude-code --plugin accProvides CQRS patterns, checklists, antipatterns, and PHP-specific guidelines for auditing command/query handlers and separation of concerns.
Applies CQRS and Event Sourcing for read/write separation, audit trails, and scalability. Use for complex domain logic, full auditability, or temporal queries.
Guides implementing CQRS to separate read and write models, optimize query performance, and build event-sourced systems.