acc-check-cqrs-alignment
1
总安装量
1
周安装量
#47279
全站排名
安装命令
npx skills add https://github.com/dykyi-roman/awesome-claude-code --skill acc-check-cqrs-alignment
Agent 安装分布
opencode
1
claude-code
1
Skill 文档
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