Vortos
CQRS

CQRS

Command and query buses with compile-time handler discovery, idempotency, transaction ownership, and projection handlers.

CQRS

Vortos provides CommandBus and QueryBus with compile-time handler auto-discovery. Mark your handler with one attribute — discovery, wiring, transaction management, event dispatch, and idempotency are all handled automatically.

Compile-Time Discovery

CommandHandlerPass and QueryHandlerPass scan all services at compile time, infer the handled class from __invoke() parameter types, and build ServiceLocator maps. At runtime, dispatch is a single map lookup — zero reflection.

Architecture

Command → CommandBus
    ├── Idempotency check (pre-built strategy map — zero reflection)
    ├── UnitOfWork::run()
    │       ├── handler(__invoke($command))
    │       ├── pullDomainEvents() from returned aggregate
    │       └── EventBus::dispatch() each event (writes to outbox)
    └── UnitOfWork::commit()
    └── markProcessed(idempotencyKey)

Query → QueryBus
    └── handler(__invoke($query)) → returns result
        (no transaction, no events, no idempotency)

Installation

composer require vortos/vortos-cqrs

Package Registration

bootstrap/app.php
use Vortos\Cqrs\DependencyInjection\CqrsPackage;

$packages = [
    new CachePackage(),       // required — idempotency store uses CacheInterface
    new CqrsPackage(),
];

Configuration

config/cqrs.php
use Vortos\Cqrs\DependencyInjection\VortosCqrsConfig;

return static function (VortosCqrsConfig $config): void {
    $config->commandBus()
        ->idempotencyStore(RedisCommandIdempotencyStore::class) // default
        ->idempotencyTtl(86400)                                 // 24 hours default
        ->strictIdempotency(false);                             // false = silent skip (default)
};

No config file needed — all settings have sensible defaults.

All Config Options

MethodDefaultDescription
->idempotencyStore(string)RedisCommandIdempotencyStore::classFQCN of CommandIdempotencyStoreInterface
->idempotencyTtl(int)86400Seconds to keep processed keys (24 hours)
->strictIdempotency(bool)falsefalse = silent skip, true = throw DuplicateCommandException

Compiler Pass Order

CommandHandlerPass  (priority 50) — builds command handler map
QueryHandlerPass    (priority 50) — builds query handler map
IdempotencyKeyPass  (priority 40) — reads command map, resolves idempotency strategies

IdempotencyKeyPass must run after CommandHandlerPass — it reads the map that CommandHandlerPass builds.

Module Overview

On this page