check-cqrs-alignment
CQRS & Event Sourcing Alignment Audit
Analyze PHP code for proper CQRS implementation and Event Sourcing compliance.
Detection Patterns
1. Command Returning Data
// 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
}
}
2. Query Modifying State
// 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());
}
}
3. Read Model Using Write Repository
// 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());
}
}
4. Non-Idempotent Projection
// 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'),
],
);
}
}
5. Event Without Version/Timestamp
// 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,
) {}
}
6. Mixed Command and Query Bus
// 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;
}
7. Event Store Without Optimistic Locking
// 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...
}
}
Grep Patterns
# 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"
Severity Classification
| 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 |
Output Format
### 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
More from dykyi-roman/awesome-claude-code
psr-overview-knowledge
PHP Standards Recommendations (PSR) overview knowledge base. Provides comprehensive reference for all accepted PSRs including PSR-1,3,4,6,7,11,12,13,14,15,16,17,18,20. Use for PSR selection decisions and compliance audits.
22detect-code-smells
Detects code smells in PHP codebases. Identifies God Class, Feature Envy, Data Clumps, Long Parameter List, Long Method, Primitive Obsession, Message Chains, Inappropriate Intimacy. Generates actionable reports with refactoring recommendations.
15clean-arch-knowledge
Clean Architecture knowledge base. Provides patterns, antipatterns, and PHP-specific guidelines for Clean Architecture and Hexagonal Architecture audits.
15ddd-knowledge
DDD architecture knowledge base. Provides patterns, antipatterns, and PHP-specific guidelines for Domain-Driven Design audits.
14testing-knowledge
Testing knowledge base for PHP 8.4 projects. Provides testing pyramid, AAA pattern, naming conventions, isolation principles, DDD testing guidelines, and PHPUnit patterns.
12bug-root-cause-finder
Root cause analysis methods for PHP bugs. Provides 5 Whys technique, fault tree analysis, git bisect guidance, and stack trace parsing.
12