-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch-index.json
More file actions
1 lines (1 loc) · 149 KB
/
search-index.json
File metadata and controls
1 lines (1 loc) · 149 KB
1
[{"content":"Using a dependency injection container is beneficial for bootstrapping Backslash applications. The number of components and their dependencies makes a container practical for managing instantiation and wiring.\nThe demo application implements its own PSR-11 container in src/Infrastructure/Container.php for demonstration purposes. In production applications, prefer using your framework\u0026rsquo;s built-in container (Laravel, Symfony, Slim, etc.) or a dedicated container library like php-di/php-di or league/container. These mature containers provide features like auto-wiring, configuration caching, and better performance.\nThis section shows how to define Backslash components as services in your dependency injection container.\nUnderstanding Backslash interfaces Backslash provides an interface for each component. These interfaces serve as contracts that define what operations each component supports, independent of their concrete implementations.\nThe core interfaces include:\nDispatcherInterface for command dispatching EventBusInterface for event publishing and subscription EventStoreInterface for event persistence and retrieval ProjectionStoreInterface for projection storage and querying RepositoryInterface for model loading and persistence SerializerInterface for object serialization StreamEnricherInterface for metadata enrichment PdoInterface for PDO connections Services depend on interfaces rather than concrete implementations, making code more flexible and testable:\nclass CourseCommandHandler { public function __construct( private RepositoryInterface $repository, // Depends on interface ) { } }\rThis approach allows you to swap implementations without changing dependent code. For testing, you can inject mock implementations; for production, you inject the real implementations configured in your container.\nService definition patterns Most containers support defining services through configuration files, PHP arrays, or using auto-wiring. The examples below use closure-based definitions compatible with most PSR-11 containers:\n// Simple service with no dependencies StreamEnricherInterface::class =\u0026gt; fn () =\u0026gt; new StreamEnricher(), // Service with container-resolved dependencies RepositoryInterface::class =\u0026gt; fn (ContainerInterface $c) =\u0026gt; new Repository( $c-\u0026gt;get(EventStoreInterface::class), $c-\u0026gt;get(EventBusInterface::class), ), // Service with configuration PdoInterface::class =\u0026gt; function () { $dsn = getenv(\u0026#39;DB_DSN\u0026#39;) ?: \u0026#39;sqlite:data/demo.sqlite\u0026#39;; return new PdoProxy(fn () =\u0026gt; new PDO($dsn)); },\rAdapt these patterns to your container\u0026rsquo;s preferred configuration format. All Backslash components should be registered as singleton services; instances should be shared and reused throughout your application.\nConfiguring the PDO connection Define the database connection as a service:\nuse Backslash\\Pdo\\PdoInterface; use Backslash\\Pdo\\PdoProxy; PdoInterface::class =\u0026gt; function () { $dsn = getenv(\u0026#39;DB_DSN\u0026#39;) ?: \u0026#39;sqlite:data/demo.sqlite\u0026#39;; $username = getenv(\u0026#39;DB_USERNAME\u0026#39;) ?: null; $password = getenv(\u0026#39;DB_PASSWORD\u0026#39;) ?: null; return new PdoProxy(fn () =\u0026gt; new PDO($dsn, $username, $password)); },\rThe PdoProxy defers the actual PDO connection until the first database operation.\nUse environment variables for database configuration to keep credentials out of code and support different environments.\nConfiguring the EventStore Define the EventStore with its adapter and middleware:\nuse Backslash\\EventNameResolver\\EventNameResolver; use Backslash\\EventNameResolver\\MatchingClassEventNameResolverAdapter; use Backslash\\EventStore\\EventStore; use Backslash\\EventStore\\EventStoreInterface; use Backslash\\PdoEventStore\\PdoEventStoreAdapter; use Backslash\\PdoEventStore\\Config as PdoEventStoreConfig; use Backslash\\Serializer\\Serializer; use Backslash\\PdoEventStore\\JsonEventSerializer; use Backslash\\PdoEventStore\\JsonIdentifiersSerializer; use Backslash\\PdoEventStore\\JsonMetadataSerializer; use Backslash\\StreamEnricher\\StreamEnricherEventStoreMiddleware; use Ramsey\\Uuid\\Uuid; EventStoreInterface::class =\u0026gt; function (ContainerInterface $c) { $eventNameResolver = new EventNameResolver(new MatchingClassEventNameResolverAdapter()); $store = new EventStore( new PdoEventStoreAdapter( $c-\u0026gt;get(PdoInterface::class), new PdoEventStoreConfig(), $eventNameResolver, new Serializer(new JsonEventSerializer($eventNameResolver)), new Serializer(new JsonIdentifiersSerializer()), new Serializer(new JsonMetadataSerializer()), fn () =\u0026gt; Uuid::uuid4()-\u0026gt;toString(), ), ); $store-\u0026gt;addMiddleware( new StreamEnricherEventStoreMiddleware( $c-\u0026gt;get(StreamEnricherInterface::class) ) ); return $store; },\rThe EventStore requires an adapter (PdoEventStoreAdapter), serializers for events, identifiers, and metadata, and a function to generate event IDs.\nAdd the StreamEnricherEventStoreMiddleware to inject metadata into events as they\u0026rsquo;re persisted.\nConfiguring the ProjectionStore Define the ProjectionStore with its adapter and middleware:\nuse Backslash\\ProjectionStore\\ProjectionStore; use Backslash\\ProjectionStore\\ProjectionStoreInterface; use Backslash\\PdoProjectionStore\\PdoProjectionStoreAdapter; use Backslash\\PdoProjectionStore\\Config as PdoProjectionStoreConfig; use Backslash\\Serializer\\SerializeFunctionSerializer; use Backslash\\CacheProjectionStoreMiddleware\\CacheProjectionStoreMiddleware; ProjectionStoreInterface::class =\u0026gt; function (ContainerInterface $c) { $store = new ProjectionStore( new PdoProjectionStoreAdapter( $c-\u0026gt;get(PdoInterface::class), new Serializer(new SerializeFunctionSerializer()), new PdoProjectionStoreConfig(), ), ); $store-\u0026gt;addMiddleware(new CacheProjectionStoreMiddleware()); return $store; },\rThe ProjectionStore requires an adapter (PdoProjectionStoreAdapter), a serializer, and configuration.\nAdd the CacheProjectionStoreMiddleware to cache projections in memory and reduce database queries.\nFor testing purposes, you can easily swap the adapter for an in-memory implementation:\nuse Backslash\\InMemoryProjectionStore\\InMemoryProjectionStoreAdapter; ProjectionStoreInterface::class =\u0026gt; function (ContainerInterface $c) { $isTestMode = (bool) getenv(\u0026#39;TESTING\u0026#39;); $adapter = $isTestMode ? new InMemoryProjectionStoreAdapter() : new PdoProjectionStoreAdapter( $c-\u0026gt;get(PdoInterface::class), new Serializer(new SerializeFunctionSerializer()), new PdoProjectionStoreConfig(), ); $store = new ProjectionStore($adapter); $store-\u0026gt;addMiddleware(new CacheProjectionStoreMiddleware()); return $store; },\rThis approach allows tests to run faster without database dependencies while using the same service definition.\nConfiguring the Repository Define the Repository with its dependencies:\nuse Backslash\\Repository\\Repository; use Backslash\\Repository\\RepositoryInterface; RepositoryInterface::class =\u0026gt; fn (ContainerInterface $c) =\u0026gt; new Repository( $c-\u0026gt;get(EventStoreInterface::class), $c-\u0026gt;get(EventBusInterface::class), ),\rThe Repository requires the EventStore for loading and persisting events, and the EventBus for publishing events after persistence.\nConfiguring the EventBus Define the EventBus with its middleware:\nuse Backslash\\EventBus\\EventBus; use Backslash\\EventBus\\EventBusInterface; use Backslash\\StreamEnricher\\StreamEnricherEventBusMiddleware; EventBusInterface::class =\u0026gt; function (ContainerInterface $c) { $bus = new EventBus(); $bus-\u0026gt;addMiddleware( new StreamEnricherEventBusMiddleware( $c-\u0026gt;get(StreamEnricherInterface::class) ) ); return $bus; },\rAdd the StreamEnricherEventBusMiddleware to inject metadata into events as they\u0026rsquo;re published, ensuring consistency with persisted events.\nEvent handler registration (subscribing event handlers to events) does not happen here; it occurs during the boot process explained in section 17.\nConfiguring the Dispatcher Define the Dispatcher with its middleware:\nuse Backslash\\CommandDispatcher\\Dispatcher; use Backslash\\CommandDispatcher\\DispatcherInterface; use Backslash\\PdoTransactionCommandDispatcherMiddleware\\PdoTransactionCommandDispatcherMiddleware; use Backslash\\ProjectionStoreTransactionCommandDispatcherMiddleware\\ProjectionStoreTransactionCommandDispatcherMiddleware; DispatcherInterface::class =\u0026gt; function (ContainerInterface $c) { $dispatcher = new Dispatcher(); $dispatcher-\u0026gt;addMiddleware( new PdoTransactionCommandDispatcherMiddleware( $c-\u0026gt;get(PdoInterface::class) ) ); $dispatcher-\u0026gt;addMiddleware( new ProjectionStoreTransactionCommandDispatcherMiddleware( $c-\u0026gt;get(ProjectionStoreInterface::class) ) ); return $dispatcher; },\rAdd the PdoTransactionCommandDispatcherMiddleware to wrap command execution in a database transaction.\nAdd the ProjectionStoreTransactionCommandDispatcherMiddleware to automatically commit projections on success and rollback on failure.\nCommand handler registration does not happen here; it occurs during the boot process explained in section 17.\nConfiguring the StreamEnricher Define the StreamEnricher implementation:\nuse Backslash\\StreamEnricher\\StreamEnricherInterface; StreamEnricherInterface::class =\u0026gt; fn () =\u0026gt; new AppStreamEnricher(),\rIf your enricher requires dependencies like authentication services or tenant management, inject them through the constructor:\nStreamEnricherInterface::class =\u0026gt; fn (ContainerInterface $c) =\u0026gt; new AppStreamEnricher( $c-\u0026gt;get(AuthenticationService::class), $c-\u0026gt;get(TenantContext::class), ),\rConfiguring handlers and projectors Define each command handler, event handler, and projector as services:\n// Command Handlers CourseCommandHandler::class =\u0026gt; fn (ContainerInterface $c) =\u0026gt; new CourseCommandHandler( $c-\u0026gt;get(RepositoryInterface::class), ), StudentCommandHandler::class =\u0026gt; fn (ContainerInterface $c) =\u0026gt; new StudentCommandHandler( $c-\u0026gt;get(RepositoryInterface::class), ), SubscriptionCommandHandler::class =\u0026gt; fn (ContainerInterface $c) =\u0026gt; new SubscriptionCommandHandler( $c-\u0026gt;get(RepositoryInterface::class), ), // Projectors CourseListProjector::class =\u0026gt; fn (ContainerInterface $c) =\u0026gt; new CourseListProjector( $c-\u0026gt;get(ProjectionStoreInterface::class), ), StudentListProjector::class =\u0026gt; fn (ContainerInterface $c) =\u0026gt; new StudentListProjector( $c-\u0026gt;get(ProjectionStoreInterface::class), ), // Processors NotificationProcessor::class =\u0026gt; fn (ContainerInterface $c) =\u0026gt; new NotificationProcessor( $c-\u0026gt;get(MailerInterface::class), ),\rEach handler definition injects its required dependencies through the constructor.\n","date":"0001-01-01","id":0,"permalink":"/docs/application-setup/defining-services/","summary":"\u003cp\u003eUsing a dependency injection container is beneficial for bootstrapping Backslash applications. The number of components\nand their dependencies makes a container practical for managing instantiation and wiring.\u003c/p\u003e","tags":[],"title":"Defining Services"},{"content":"Middleware provides a powerful way to add cross-cutting concerns to Backslash components. It follows an onion layer model, allowing logic to be executed before and after core operations.\nUnderstanding middleware Middleware wraps component operations in layers. Each middleware can:\nExecute logic before delegating to the next layer Execute logic after the next layer completes Transform inputs or outputs Handle errors or side effects The onion layer model ensures symmetric execution:\nOuter Layer → Middle Layer → Inner Layer → [Core] → Inner Layer → Middle Layer → Outer Layer\rMultiple Backslash core components support middleware extension:\nDispatcher EventBus EventNameResolver EventStore ProjectionStore Repository Serializer (dependency of ProjectionStore and EventStore for serializing projections and events to text) CommandDispatcher middleware Dispatcher middleware wraps command dispatch. Common use cases include logging commands or handling errors:\nuse Backslash\\CommandDispatcher\\DispatcherInterface; use Backslash\\CommandDispatcher\\MiddlewareInterface; class LoggingCommandDispatcherMiddleware implements MiddlewareInterface { public function __construct( private LoggerInterface $logger, ) { } public function dispatch(object $command, DispatcherInterface $next): void { $this-\u0026gt;logger-\u0026gt;info(\u0026#39;Dispatching command\u0026#39;, [ \u0026#39;command\u0026#39; =\u0026gt; $command::class, ]); try { $next-\u0026gt;dispatch($command); $this-\u0026gt;logger-\u0026gt;info(\u0026#39;Command completed successfully\u0026#39;); } catch (Throwable $e) { $this-\u0026gt;logger-\u0026gt;error(\u0026#39;Command failed\u0026#39;, [ \u0026#39;error\u0026#39; =\u0026gt; $e-\u0026gt;getMessage(), ]); throw $e; } } }\rRegister it with the dispatcher:\n$dispatcher-\u0026gt;addMiddleware(new LoggingCommandDispatcherMiddleware($logger));\rEventStore middleware EventStore middleware wraps access to the storage adapter.\nuse Backslash\\EventStore\\MiddlewareInterface; use Backslash\\EventStore\\EventStoreInterface; use Backslash\\Event\\RecordedEventStream; use Backslash\\EventStore\\Query\\QueryInterface; class StreamEnricherEventStoreMiddleware implements MiddlewareInterface { public function __construct( private StreamEnricherInterface $enricher, ) { } public function append( RecordedEventStream $stream, ?QueryInterface $concurrencyCheck, ?int $expectedSequence, EventStoreInterface $next ): void { $enrichedStream = $this-\u0026gt;enricher-\u0026gt;enrich($stream); $next-\u0026gt;append($enrichedStream, $concurrencyCheck, $expectedSequence); } public function fetch( ?QueryInterface $query, int $fromSequence, EventStoreInterface $next ): StoredRecordedEventStream { return $next-\u0026gt;fetch($query, $fromSequence); } public function inspect(InspectorInterface $inspector, EventStoreInterface $next): void { $next-\u0026gt;inspect($inspector); } public function purge(EventStoreInterface $next): void { $next-\u0026gt;purge(); } }\rRegister it with the EventStore:\n$eventStore-\u0026gt;addMiddleware(new StreamEnricherEventStoreMiddleware($enricher));\rEventBus middleware EventBus middleware wraps event publishing. Common use cases include enriching events before broadcasting them:\nuse Backslash\\EventBus\\MiddlewareInterface; use Backslash\\EventBus\\EventBusInterface; use Backslash\\Event\\RecordedEventStream; class StreamEnricherEventBusMiddleware implements MiddlewareInterface { public function __construct( private StreamEnricherInterface $enricher, ) { } public function publish( RecordedEventStream $stream, EventBusInterface $next ): void { $enrichedStream = $this-\u0026gt;enricher-\u0026gt;enrich($stream); $next-\u0026gt;publish($enrichedStream); } }\rRegister it with the EventBus:\n$eventBus-\u0026gt;addMiddleware(new StreamEnricherEventBusMiddleware($enricher));\rProjectionStore middleware ProjectionStore middleware wraps access to the storage adapter.\nuse Backslash\\ProjectionStore\\MiddlewareInterface; use Backslash\\ProjectionStore\\ProjectionStoreInterface; use Backslash\\Projection\\ProjectionInterface; class LoggingProjectionStoreMiddleware implements MiddlewareInterface { public function __construct( private LoggerInterface $logger, ) { } public function find(string $id, string $class, ProjectionStoreInterface $next): ProjectionInterface { $this-\u0026gt;logger-\u0026gt;debug(\u0026#39;Loading projection\u0026#39;, [\u0026#39;id\u0026#39; =\u0026gt; $id, \u0026#39;class\u0026#39; =\u0026gt; $class]); return $next-\u0026gt;find($id, $class); } public function has(string $id, string $class, ProjectionStoreInterface $next): bool { return $next-\u0026gt;has($id, $class); } public function store(ProjectionInterface $projection, ProjectionStoreInterface $next): void { $this-\u0026gt;logger-\u0026gt;debug(\u0026#39;Storing projection\u0026#39;, [\u0026#39;class\u0026#39; =\u0026gt; $projection::class]); $next-\u0026gt;store($projection); } // Implement other required methods... }\rRepository middleware Repository middleware wraps model loading and changes persistence. Common use cases include modifying queries to apply additional conditions, such as tenant scoping in multi-tenant applications.\nThe following example shows a middleware that adds tenant filtering to all queries, avoiding the need to repeat this condition in every query throughout the application:\nuse Backslash\\Repository\\MiddlewareInterface; use Backslash\\Repository\\RepositoryInterface; use Backslash\\Model\\ModelInterface; use Backslash\\EventStore\\Query\\QueryInterface; use Backslash\\EventStore\\Query\\Metadata; class TenantScopingRepositoryMiddleware implements MiddlewareInterface { public function __construct( private string $tenantId, ) { } public function loadModel( string $modelClass, ?QueryInterface $query, RepositoryInterface $next ): ModelInterface { // Add tenant filtering to query $scopedQuery = $query?-\u0026gt;and(Metadata::is(\u0026#39;tenant_id\u0026#39;, $this-\u0026gt;tenantId)); return $next-\u0026gt;loadModel($modelClass, $scopedQuery); } public function storeChanges(ModelInterface $model, RepositoryInterface $next): void { $next-\u0026gt;storeChanges($model); } }\rBuilt-in middleware Backslash provides several ready-to-use middleware:\nCacheProjectionStoreMiddleware - Caches loaded projections in memory to avoid unnecessary storage communication, significantly improving performance when projections are accessed multiple times:\nuse Backslash\\CacheProjectionStoreMiddleware\\CacheProjectionStoreMiddleware; $projectionStore-\u0026gt;addMiddleware(new CacheProjectionStoreMiddleware());\rPdoTransactionCommandDispatcherMiddleware - Creates a PDO transaction for command dispatch; rolls back if an exception occurs to prevent new events from being written to the database:\nuse Backslash\\PdoTransactionCommandDispatcherMiddleware\\PdoTransactionCommandDispatcherMiddleware; $dispatcher-\u0026gt;addMiddleware(new PdoTransactionCommandDispatcherMiddleware($pdo));\rProjectionStoreTransactionCommandDispatcherMiddleware - Calls commit() on ProjectionStore after command processing completes successfully:\nuse Backslash\\ProjectionStoreTransactionCommandDispatcherMiddleware\\ProjectionStoreTransactionCommandDispatcherMiddleware; $dispatcher-\u0026gt;addMiddleware( new ProjectionStoreTransactionCommandDispatcherMiddleware($projectionStore) );\rStreamEnricherEventBusMiddleware - Enriches events before publishing to EventBus:\nuse Backslash\\StreamEnricher\\StreamEnricherEventBusMiddleware; $eventBus-\u0026gt;addMiddleware(new StreamEnricherEventBusMiddleware($enricher));\rStreamEnricherEventStoreMiddleware - Enriches events before persisting to EventStore:\nuse Backslash\\StreamEnricher\\StreamEnricherEventStoreMiddleware; $eventStore-\u0026gt;addMiddleware(new StreamEnricherEventStoreMiddleware($enricher));\rMiddleware execution order Middleware executes in reverse order of registration (LIFO - Last In, First Out). The last middleware added executes first:\n$dispatcher-\u0026gt;addMiddleware($logging); // Executes third (innermost) $dispatcher-\u0026gt;addMiddleware($validation); // Executes second $dispatcher-\u0026gt;addMiddleware($transaction); // Executes first (outermost) When a command is dispatched:\nTransaction middleware starts transaction Validation middleware validates command Logging middleware logs the command Command handler executes Logging middleware logs completion Validation middleware completes Transaction middleware commits This onion-layer pattern ensures middleware executes symmetrically before and after the core operation.\nHere\u0026rsquo;s a concrete example using Backslash\u0026rsquo;s built-in middleware:\nuse Backslash\\ProjectionStoreTransactionCommandDispatcherMiddleware\\ProjectionStoreTransactionCommandDispatcherMiddleware; use Backslash\\PdoTransactionCommandDispatcherMiddleware\\PdoTransactionCommandDispatcherMiddleware; // Order matters: PDO transaction wraps everything $dispatcher-\u0026gt;addMiddleware(new LoggingMiddleware($logger)); // Innermost $dispatcher-\u0026gt;addMiddleware(new ProjectionStoreTransactionCommandDispatcherMiddleware($projectionStore)); // Middle $dispatcher-\u0026gt;addMiddleware(new PdoTransactionCommandDispatcherMiddleware($pdo)); // Outermost Execution flow:\nPDO transaction begins ProjectionStore prepares for changes Logging records command Command executes, events are persisted Logging records completion ProjectionStore commits buffered projections PDO transaction commits Inner middleware By default, addMiddleware() adds a new outer layer to the onion. If you need a middleware to execute closest to the core (innermost layer), use addInnerMiddleware() instead:\n$dispatcher-\u0026gt;addMiddleware($transaction); // Outer layer $dispatcher-\u0026gt;addMiddleware($logging); // Middle layer $dispatcher-\u0026gt;addInnerMiddleware($tracing); // Inner layer (closest to core) Execution flow:\nTransaction → Logging → Tracing → [Core] → Tracing → Logging → Transaction\rThis is useful when a middleware must always run closest to the core, regardless of what other middleware has already been registered. For example, the Scenario testing component uses addInnerMiddleware() to ensure its tracing middleware captures exactly what the core produces, even when the host application has added its own middleware to the EventBus or ProjectionStore.\nAll components that support middleware provide both addMiddleware() and addInnerMiddleware().\nBest practices Keep middleware focused. Each middleware should handle a single concern; avoid creating god middleware that handles multiple responsibilities.\nMake middleware reusable. Write middleware that works with any component of its type, not just specific use cases.\nConsider execution order. Add middleware in the correct order based on dependencies; transactions should wrap validation, validation should wrap logging, etc.\nHandle exceptions appropriately. Middleware can catch and transform exceptions, but be careful not to swallow important errors that should propagate to callers.\nTest middleware independently. Middleware should be testable in isolation from the components they wrap.\nDocument side effects. If middleware modifies state or has side effects, document this clearly in comments or documentation.\nUse built-in middleware when available. Backslash provides common middleware implementations; use them instead of rolling your own.\n","date":"0001-01-01","id":1,"permalink":"/docs/customization/extending-with-middleware/","summary":"\u003cp\u003eMiddleware provides a powerful way to add cross-cutting concerns to Backslash components. It follows an onion layer\nmodel, allowing logic to be executed before and after core operations.\u003c/p\u003e","tags":[],"title":"Extending with Middleware"},{"content":"","date":"0001-01-01","id":2,"permalink":"/docs/getting-started/","summary":"","tags":[],"title":"Getting Started"},{"content":"Backslash is a modern and opinionated PHP library for building event-sourced applications with CQRS. It is fully compliant with the Dynamic Consistency Boundary specification.\nUnlike most event sourcing libraries built around aggregates with rigid boundaries, Backslash allows you to define consistency boundaries on the fly based on the decision you\u0026rsquo;re making, not based on predetermined aggregate structures.\nImportant note\nBackslash has been in continuous production for over seven years at the First Nations of Quebec and Labrador Health and Social Services Commission. Although it was initially developed to meet the organization’s specific needs, Backslash follows a generic event sourcing approach that remains true to the core principles of the pattern.\nCore capabilities Dynamic consistency boundaries. Define which events matter for each decision. No fixed aggregates; just queries that fetch exactly what you need.\nBuilt-in persistence. PDO-compatible adapters for storing events and projections are included. Supports MySQL and SQLite out of the box.\nSynchronous projections. Projections are updated immediately within the same transaction as command execution, ensuring read-your-writes consistency.\nFramework agnostic. Works with any PHP framework or runs standalone. Use it with Laravel, Symfony, Slim, or your own custom setup.\nEvent replay and projection rebuilding. Reconstruct projections from scratch by replaying historical events, enabling schema changes and bug fixes.\nOptimistic concurrency control. Prevent race conditions with built-in version checking that detects concurrent modifications.\nExtensible through middleware. Add logging, validation, transactions, or any cross-cutting concern by wrapping core operations with middleware.\nStream enrichment. Enhance events with contextual metadata like correlation IDs, user information, or tenant context as they flow through the system.\nBDD-style testing. Write expressive tests using the Scenario component with given-when-then syntax that reads like specifications.\nPrerequisites While not strictly required, familiarity with event sourcing and CQRS fundamentals is strongly recommended. If these concepts are new to you, start with these resources:\nMartin Fowler on Event Sourcing Event Store – What Is Event Sourcing? Greg Young\u0026rsquo;s Original CQRS Introduction (2007) Microsoft Docs: CQRS Pattern Installing Backslash Install Backslash using Composer:\ncomposer require backslashphp/backslash\rExploring the demo application The examples in this documentation come from the demo application, a simple course enrollment system where students can subscribe to courses with capacity limits. The demo demonstrates how to define events, build decision-making models, react with event handlers, and maintain projections.\nThe domain chosen for the demo application is inspired by Sara Pellegrini\u0026rsquo;s blog series \u0026ldquo;Kill Aggregate\u0026rdquo;.\nThe demo is intentionally simple to keep focus on Backslash concepts rather than domain complexity. Real applications will have more sophisticated domain logic, but the patterns remain the same.\nDeveloping a Backslash system Building an event-sourced application with Backslash involves several key steps:\nDefine events that represent facts about what happened in your system Build models that replay events to understand current state and make decisions Write queries to select which events are relevant for each decision Create command handlers that load models, execute business logic, and persist changes Set up event handlers that react to events by updating projections or triggering side effects Backslash handles command routing, event persistence, concurrency control, and event publishing; you focus on expressing your domain logic clearly.\n","date":"0001-01-01","id":3,"permalink":"/docs/getting-started/introduction/","summary":"\u003cp\u003eBackslash is a modern and opinionated PHP library for building event-sourced applications with CQRS. It is fully\ncompliant with the \u003ca href=\"https://dcb.events/specification/\"\u003eDynamic Consistency Boundary specification\u003c/a\u003e.\u003c/p\u003e","tags":[],"title":"Introduction"},{"content":"Rebuilding projections is a routine task in event-sourced applications. This capability allows you to reconstruct read models from scratch by replaying the complete event history.\nBackslash does not provide a built-in component specifically for rebuilding projections; each application has unique requirements and should implement rebuilding according to its specific needs. However, Backslash provides essential tools that facilitate the rebuild process, such as the Inspector for replaying events, the ProjectionStore for managing projection lifecycle, and the EventBus for routing events to projectors.\nUnderstanding when to rebuild Rebuild projections when:\nCreating new projections: New read models need historical data from existing events Modifying projection logic: Changes to how projections interpret events require rebuilding with the updated logic Optimizing projection structure: Performance improvements or schema changes necessitate reconstruction Fixing projection bugs: Corrected projector logic must be applied to all historical events Migrating projection storage: Moving projections to different storage systems requires rebuilding in the new location Treating projections as disposable Because events are the source of truth, projections are disposable and easily reconstructed. Events represent what actually happened in your system; projections are merely interpretations of those events. While projections may occasionally contain errors due to bugs in projector logic, events always tell the truth.\nThis fundamental principle means you can confidently delete and rebuild projections whenever needed. The ability to rebuild projections is what makes CQRS practical for evolving systems.\nDistinguishing projectors from processors Before rebuilding, understand the critical difference between two types of event handlers:\nProjectors update projections by reading events and storing read models. They are idempotent and side-effect-free beyond projection updates. Examples include:\nBuilding course lists from CourseDefinedEvent Updating student enrollments from StudentSubscribedToCourseEvent Maintaining dashboard metrics from various events Processors trigger side effects like sending notifications, calling external APIs, or publishing to message queues. They should not execute during rebuilds. Examples include:\nSending confirmation emails when students subscribe to courses Notifying external systems of state changes Publishing events to message brokers During rebuilds, register only projectors; never register processors. Processors should execute only once when events first occur, not during historical replays.\nUnderstanding EventStore and Inspector Normal operations use the Repository to load models and persist changes. Rebuilding requires direct access to the EventStore and EventBus using the Inspector component.\nThe EventStore provides an append-only, queryable log of all events. The Inspector iterates through stored events and publishes them to the EventBus:\nuse Backslash\\StreamPublishingInspection\\Inspector; $inspector = new Inspector($eventBus); $eventStore-\u0026gt;inspect($inspector);\rThis replays every event in the EventStore, triggering all registered event handlers as if the events were just published.\nFiltering events during replay The Inspector optionally accepts a Query as its second constructor argument to retrieve only a subset of events from the EventStore. This is particularly useful when rebuilding a single projector; you can pass a query that selects only events relevant to that projector:\nuse Backslash\\StreamPublishingInspection\\Inspector; use Backslash\\EventStore\\Query\\EventClass; // Define which events the projector needs $relevantEvents = [ CourseDefinedEvent::class, CourseCapacityChangedEvent::class, StudentSubscribedToCourseEvent::class, ]; // Create a query filtering for these events only $query = EventClass::in($relevantEvents); // Inspector will only replay these specific events $inspector = new Inspector($eventBus, $query); $eventStore-\u0026gt;inspect($inspector);\rWithout a query parameter, the Inspector replays all events in the EventStore. When rebuilding all projections, omit the query; when rebuilding specific projections, use EventClass::in() to filter for only the necessary events.\nTracking rebuild progress The Inspector optionally accepts third and fourth constructor arguments as closures for tracking rebuild progress. The first closure executes before dispatching each event to the EventBus; the second executes after dispatch:\n$eventCount = 0; $totalEvents = $eventStore-\u0026gt;count(); // Hypothetical method $beforeDispatch = function (RecordedEvent $recordedEvent) use (\u0026amp;$eventCount, $totalEvents) { $eventCount++; echo sprintf( \u0026#34;Processing event %d/%d: %s\\n\u0026#34;, $eventCount, $totalEvents, $recordedEvent-\u0026gt;getEvent()::class ); }; $afterDispatch = function (RecordedEvent $recordedEvent) { // Log completion, update progress bar, etc. }; $inspector = new Inspector($eventBus, $query, $beforeDispatch, $afterDispatch); $eventStore-\u0026gt;inspect($inspector);\rThese callbacks are particularly useful for outputting progress to the console, calculating completion percentages, or logging rebuild metrics.\nThe serial rebuild process Rebuilding follows these steps:\nBootstrap the application with only projectors registered; exclude all processors to prevent side effects Delete existing projections by calling purge() on the ProjectionStore Disable stream enricher if your application uses one; metadata enrichment must not occur during replays Load all events chronologically from the EventStore and publish them to the EventBus using Inspector Commit rebuilt projections by calling commit() on the ProjectionStore The serial approach processes events sequentially, looping through all events one after the other in a single PHP process. Here\u0026rsquo;s a complete serial rebuild implementation:\n\u0026lt;?php declare(strict_types=1); use Backslash\\CommandDispatcher\\DispatcherInterface; use Backslash\\EventBus\\EventBusInterface; use Backslash\\EventStore\\EventStoreInterface; use Backslash\\ProjectionStore\\ProjectionStoreInterface; use Backslash\\StreamPublishingInspection\\Inspector; use Demo\\UI\\Projection\\CourseList\\CourseListProjector; use Demo\\UI\\Projection\\StudentList\\StudentListProjector; use Psr\\Container\\ContainerInterface; /** @var ContainerInterface $container */ $container = require __DIR__ . \u0026#39;/../bootstrap.php\u0026#39;; /** @var EventStoreInterface $eventStore */ $eventStore = $container-\u0026gt;get(EventStoreInterface::class); /** @var ProjectionStoreInterface $projectionStore */ $projectionStore = $container-\u0026gt;get(ProjectionStoreInterface::class); // Create a dedicated EventBus with only projectors $eventBus = new EventBus(); // Register projectors (not processors) $courseListProjector = new CourseListProjector($projectionStore); $studentListProjector = new StudentListProjector($projectionStore); $eventBus-\u0026gt;subscribe(CourseDefinedEvent::class, $courseListProjector); $eventBus-\u0026gt;subscribe(CourseCapacityChangedEvent::class, $courseListProjector); $eventBus-\u0026gt;subscribe(StudentSubscribedToCourseEvent::class, $courseListProjector); $eventBus-\u0026gt;subscribe(StudentUnsubscribedFromCourseEvent::class, $courseListProjector); $eventBus-\u0026gt;subscribe(StudentRegisteredEvent::class, $courseListProjector); $eventBus-\u0026gt;subscribe(CourseDefinedEvent::class, $studentListProjector); $eventBus-\u0026gt;subscribe(StudentRegisteredEvent::class, $studentListProjector); $eventBus-\u0026gt;subscribe(StudentSubscribedToCourseEvent::class, $studentListProjector); $eventBus-\u0026gt;subscribe(StudentUnsubscribedFromCourseEvent::class, $studentListProjector); // Step 1: Purge existing projections $projectionStore-\u0026gt;purge(); $projectionStore-\u0026gt;commit(); echo \u0026#34;Purged existing projections\\n\u0026#34;; // Step 2: Replay all events $inspector = new Inspector($eventBus); $eventStore-\u0026gt;inspect($inspector); echo \u0026#34;Replayed all events\\n\u0026#34;; // Step 3: Commit rebuilt projections $projectionStore-\u0026gt;commit(); echo \u0026#34;Rebuild complete\\n\u0026#34;;\rThis serial approach is effective for getting started but processes events sequentially in a single PHP process.\nDisabling stream enrichment If your application uses a stream enricher, disable it during rebuilds to prevent metadata enrichment. Stream enrichers typically add contextual information like user IDs, tenant identifiers, or correlation IDs to events as they occur.\nDuring rebuilds, you\u0026rsquo;re replaying historical events that already contain their original metadata. Re-enriching them would:\nAdd incorrect contextual data from the rebuild process rather than the original context Potentially modify event metadata in unintended ways Cause unnecessary processing overhead Disable enrichment by not registering the enricher middleware when bootstrapping your rebuild script:\n// Normal application bootstrap registers enricher middleware $eventStore-\u0026gt;addMiddleware(new StreamEnricherEventStoreMiddleware($enricher)); $eventBus-\u0026gt;addMiddleware(new StreamEnricherEventBusMiddleware($enricher)); // Rebuild script omits enricher middleware entirely // Just use EventStore and EventBus without enricher middleware Alternatively, implement methods like enable() and disable() on your stream enricher to toggle activation:\nclass StreamEnricher implements StreamEnricherInterface { private bool $enabled = true; public function enable(): void { $this-\u0026gt;enabled = true; } public function disable(): void { $this-\u0026gt;enabled = false; } public function enrich(EventStreamInterface $stream): EventStreamInterface { if (!$this-\u0026gt;enabled) { return $stream; } // Enrichment logic here } } // In rebuild script $enricher-\u0026gt;disable(); // After rebuild $enricher-\u0026gt;enable();\rThis approach allows you to keep the enricher middleware registered while controlling when enrichment occurs.\nImplementing parallel rebuilds For large event stores, parallel rebuilding improves performance by running each projector in a separate PHP process. This approach reduces memory consumption and CPU usage by distributing work across multiple workers.\nOne suggested parallel rebuild strategy involves two scripts:\nMain coordinator script that:\nDeletes all existing projections Loops through all projectors Launches a background process for each projector, passing the projector class name as an argument Waits for all background processes to complete Worker script that:\nReceives the projector class name as an argument Determines which events the projector is interested in Creates a query filtering for those specific events using EventClass::in() Uses the Inspector with this query to replay only the relevant events Creates a dedicated EventBus and registers only that projector Instructs the EventStore to inspect using the filtered Inspector Commits the rebuilt projections for that projector only This approach allows each projector to process only its relevant events independently, enabling true parallel execution where multiple projectors rebuild simultaneously in separate PHP processes.\nEach worker performs its own commit(), ensuring that projections are persisted independently.\nImportant: This parallel method requires projectors to be fully autonomous. Projectors must not depend on projections built by other projectors, as those projections may not be available when needed during concurrent execution. Each projector should derive all necessary information solely from events. When projectors are autonomous, there is no risk of conflicts between workers since each projector manages distinct projections.\nFiltering events for parallel rebuilds When implementing the worker script, use the Inspector with a query to replay only events relevant to the specific projector. Adding a static method to each projector that returns its subscribed events simplifies worker scripts and centralizes event subscription knowledge:\nclass CourseListProjector implements EventHandlerInterface { use EventHandlerTrait; public static function getSubscribedEvents(): array { return [ CourseDefinedEvent::class, CourseCapacityChangedEvent::class, StudentSubscribedToCourseEvent::class, StudentUnsubscribedFromCourseEvent::class, ]; } // Handler methods... }\rThe worker script can then call this method to determine which events to load:\nuse Backslash\\EventStore\\Query\\EventClass; use Backslash\\StreamPublishingInspection\\Inspector; // Worker receives projector class name as argument $projectorClass = $argv[1]; // e.g., CourseListProjector::class // Get subscribed events from projector $eventClasses = $projectorClass::getSubscribedEvents(); // Create query to filter for relevant events only $query = EventClass::in($eventClasses); // Create and register projector $eventBus = new EventBus(); $projector = new $projectorClass($projectionStore); foreach ($eventClasses as $eventClass) { $eventBus-\u0026gt;subscribe($eventClass, $projector); } // Create Inspector with the filtering query $inspector = new Inspector($eventBus, $query); // Replay only the filtered events $eventStore-\u0026gt;inspect($inspector); // Commit the rebuilt projections $projectionStore-\u0026gt;commit();\rThis pattern ensures each worker processes only its necessary events, avoiding unnecessary event handling and improving rebuild performance. The Inspector with the query guarantees that only relevant events are replayed through the EventBus.\nBest practices Make the application unavailable during rebuilds. The application should not be accessible while rebuilding projections to prevent inconsistent reads and potential data corruption. For live rebuilds without downtime, rebuild projections in separate storage and ensure new events created during the rebuild are processed after completion; this requires careful orchestration and is beyond the scope of basic rebuilds.\nSeparate projectors from processors. Maintain clear boundaries between event handlers that update projections and those that trigger side effects; this separation is essential for safe rebuilds.\nTest rebuild logic before production. Verify your rebuild implementation works correctly on a copy of production data before executing against live projections.\nSchedule rebuilds during maintenance windows. Rebuilding can be resource-intensive for large event stores; schedule rebuilds during low-traffic periods to minimize impact.\nMonitor rebuild progress. For large event stores, add logging to track rebuild progress and identify potential issues early; consider logging after every N events processed.\nUse selective rebuilds when possible. Rebuild only affected projections rather than all projections to save time and resources; create temporary EventBus instances with only the necessary projectors.\nConsider parallel rebuilds for scale. As your event store grows, parallel rebuilds become increasingly valuable for reducing rebuild time and resource consumption.\nBackup before rebuilding. Consider backing up projection data before rebuilding in case you need to roll back; this is especially important for production systems.\nKeep projectors idempotent. Design projectors to produce the same result regardless of how many times events are replayed; this ensures reliable rebuilding and recovery.\nDocument your rebuild process. Maintain clear documentation of your rebuild procedures, including when to rebuild, how to execute rebuilds, and expected duration for different projection sets.\n","date":"0001-01-01","id":4,"permalink":"/docs/maintenance/rebuilding-projections/","summary":"\u003cp\u003eRebuilding projections is a routine task in event-sourced applications. This capability allows you to reconstruct read\nmodels from scratch by replaying the complete event history.\u003c/p\u003e","tags":[],"title":"Rebuilding Projections"},{"content":"Testing event-sourced applications requires specific infrastructure setup. This section shows how to configure PHPUnit and create a base test case for your Backslash application.\nInstalling PHPUnit Add PHPUnit to your project:\ncomposer require --dev phpunit/phpunit\rCreating phpunit.xml Create a phpunit.xml configuration file in your project root:\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;phpunit bootstrap=\u0026#34;vendor/autoload.php\u0026#34;\u0026gt; \u0026lt;testsuites\u0026gt; \u0026lt;testsuite name=\u0026#34;Application Tests\u0026#34;\u0026gt; \u0026lt;directory\u0026gt;tests\u0026lt;/directory\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;/testsuites\u0026gt; \u0026lt;php\u0026gt; \u0026lt;env name=\u0026#34;TESTING\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;PDO_DSN\u0026#34; value=\u0026#34;sqlite:memory:\u0026#34;/\u0026gt; \u0026lt;/php\u0026gt; \u0026lt;/phpunit\u0026gt;\rCreating a base test case Create an abstract base test case that sets up the infrastructure components needed for testing. The primary goal is to instantiate a Scenario object, which will be used by test scenarios explained in section 13.\nThe way you obtain Backslash components depends heavily on how your application is structured. Most PHP applications use a dependency injection container. Here\u0026rsquo;s an example using a PSR-11 container:\n\u0026lt;?php declare(strict_types=1); namespace Tests; use Backslash\\CommandDispatcher\\DispatcherInterface; use Backslash\\EventBus\\EventBusInterface; use Backslash\\PdoEventStore\\Config; use Backslash\\PdoEventStore\\Driver; use Backslash\\ProjectionStore\\ProjectionStoreInterface; use Backslash\\Scenario\\AssertionsTrait; use Backslash\\Scenario\\Scenario; use PDO; use PHPUnit\\Framework\\TestCase as PHPUnitTestCase; use Psr\\Container\\ContainerInterface; abstract class TestCase extends PHPUnitTestCase { use AssertionsTrait; protected Scenario $scenario; private ContainerInterface $container; public function setUp(): void { parent::setUp(); // Bootstrap your DI container $this-\u0026gt;container = require __DIR__ . \u0026#39;/../bootstrap.php\u0026#39;; // Get PDO SQLite instance – Make sure your DI container uses the PDO_DSN variable in phpunit.xml when running tests! $pdo = $this-\u0026gt;container-\u0026gt;get(PDO::class); // Create table for the EventStore $pdo-\u0026gt;exec(Driver::SQLITE-\u0026gt;buildCreateTableStatement(new Config())); // Create Scenario instance with required dependencies $this-\u0026gt;scenario = new Scenario( $this-\u0026gt;container-\u0026gt;get(EventBusInterface::class), $this-\u0026gt;container-\u0026gt;get(DispatcherInterface::class), // Your DI container must use the InMemoryProjectionStoreAdapter when running tests. $this-\u0026gt;container-\u0026gt;get(ProjectionStoreInterface::class), $this-\u0026gt;container-\u0026gt;get(EventStoreInterface::class) ); // Set up initial test data $this-\u0026gt;createInitialTestData(); } private function createInitialTestData(): void { $dispatcher = $this-\u0026gt;container-\u0026gt;get(DispatcherInterface::class); // Dispatch commands to generate data used by test scenarios // Example: $dispatcher-\u0026gt;dispatch(new RegisterUserCommand(\u0026#39;user-1\u0026#39;, \u0026#39;John\u0026#39;, \u0026#39;Smith\u0026#39;)); } }\rThis base test case provides:\nContainer bootstrap: Loads the application\u0026rsquo;s dependency injection container Database setup: Creates EventStore tables using the SQLite driver; Driver::SQLITE generates the appropriate CREATE TABLE statement, while Config allows customization of table and column names if needed In-memory SQLite: Fast and isolated; each test gets a fresh database without external dependencies Scenario instance: Ready-to-use Scenario with EventBus, Dispatcher, and ProjectionStore AssertionsTrait: Provides scenario-specific assertions like assertPublishedEventsContain(), assertPublishedEventsCount(), and assertUpdatedProjectionsContain(), which will be covered in section 13 Initial test data: Optional createInitialTestData() method to set up common fixtures shared across tests; use this to create baseline data like system users or default configurations, avoiding duplication in individual tests ","date":"0001-01-01","id":5,"permalink":"/docs/testing/setting-up-tests/","summary":"\u003cp\u003eTesting event-sourced applications requires specific infrastructure setup. This section shows how to configure PHPUnit\nand create a base test case for your Backslash application.\u003c/p\u003e","tags":[],"title":"Setting Up Tests"},{"content":"Command Query Responsibility Segregation (CQRS) is a pattern that separates read and write operations into distinct models. Instead of using the same model for both updates and queries, CQRS uses different models optimized for their specific purposes.\nSeparating write and read sides Write side (Commands):\nProcesses commands that express intent to change state Uses models that enforce business rules Records events when operations succeed Read side (Queries):\nProvides optimized views of current state Uses projections denormalized for specific queries Updated by reacting to events from the write side In Backslash:\nCommands → Handlers → Models → Events Events → Event Handlers → Projections Benefits of CQRS Independent optimization. Write and read models can be optimized separately; complex business rules don\u0026rsquo;t complicate queries, and query optimization doesn\u0026rsquo;t constrain domain logic.\nDenormalized views. Projections can be structured however queries need them, with precomputed values and joined data for fast reads.\nClear separation of concerns. Business logic lives in models; query logic lives in projections. Each has a single, focused responsibility.\n","date":"0001-01-01","id":6,"permalink":"/docs/cqrs/understanding-cqrs/","summary":"\u003cp\u003eCommand Query Responsibility Segregation (CQRS) is a pattern that separates read and write operations into distinct\nmodels. Instead of using the same model for both updates and queries, CQRS uses different models optimized for their\nspecific purposes.\u003c/p\u003e","tags":[],"title":"Understanding CQRS"},{"content":"Backslash is designed around the core principles of event sourcing and CQRS. Its modular, lightweight, and framework-agnostic architecture allows for gradual adoption or integration into existing systems.\nCore components are provided by Backslash and handle the infrastructure behind event sourcing and CQRS.\nApplication components are written by the developer to implement use cases and domain behavior.\nComponent Core Application Role Events ✓ Represent facts that have occurred within the system. Models ✓ Replay events to hold current state and make decisions that record new events. Queries ✓ Define how to retrieve past events relevant to a model. Repository ✓ Persists model changes to the EventStore and publishes new events on the EventBus. EventNameResolver ✓ Converts event FQCNs to short names and vice versa. EventStore ✓ Stores events as an append-only, queryable log. EventBus ✓ Delivers published events to event handlers. Event handlers ✓ Respond to events to update projections or trigger side effects. Projections ✓ Read-optimized views of the system\u0026rsquo;s current state. ProjectionStore ✓ Persists and retrieves projections. Commands ✓ Express an intention or request to perform an operation on the system. Command handlers ✓ Contain the logic to process commands and update the system accordingly. Dispatcher ✓ Routes commands to their respective handlers. Scenario ✓ Provides helpers for writing BDD-style tests using PHPUnit. Testing scenarios ✓ Define given/when/then test flows using the Scenario test helpers. PdoProxy ✓ Delays the creation of a PDO connection until it\u0026rsquo;s needed. Storage adapters ✓ Provide PDO-based persistence for EventStore and ProjectionStore. Serializer ✓ Handles the serialization and deserialization of events and projections. Middlewares ✓ ✓ Add custom logic around core components operations. Stream enrichers ✓ Modify or enhance events going through EventStore and EventBus. Understanding how it works Backslash separates write operations (commands) from read operations (queries) using the CQRS pattern. The write side and read side operate independently, connected only through events.\nWrite side flow When you execute a command, here\u0026rsquo;s what happens:\nCommand dispatch: The Dispatcher receives a command and routes it to the registered command handler Query building: The handler builds a query to determine which events are needed by the model Event loading: The Repository fetches matching events from the EventStore State reconstruction: Events are replayed into the model to rebuild its current state Business decision: The model executes business logic and records new events Event persistence: The Repository appends new events to the EventStore Event publishing: New events are published to the EventBus Read side flow When events are published, the read side reacts:\nEvent notification: The EventBus delivers events to subscribed event handlers Projection update: Event handlers load projections from the ProjectionStore State modification: Projections are updated based on event data Projection persistence: Updated projections are stored back to the ProjectionStore This separation ensures that complex business rules don\u0026rsquo;t complicate queries, and query optimization doesn\u0026rsquo;t constrain domain logic.\nExtensibility Backslash provides two mechanisms for extending functionality:\nMiddlewares wrap core operations (command dispatch, event storage, projection updates) to add cross-cutting concerns like logging, validation, or transactions.\nStream enrichers modify events as they flow through the system, adding metadata such as correlation IDs, user context, or timestamps.\nHandling concurrency Backslash uses optimistic concurrency control to prevent race conditions when writing events to the EventStore. The PdoEventStoreAdapter enforces this using a conditional append in SQL that checks the expected number of events in a stream before appending new ones. If another process has modified the stream in the meantime, a concurrency exception is thrown, allowing you to retry the operation with the latest state.\nWhat\u0026rsquo;s not included Backslash focuses exclusively on event sourcing and CQRS infrastructure. It intentionally does not provide:\nHTTP routing or web framework components User interface frameworks or templating Process managers or sagas for long-running workflows Message queue integration (though you can add this via middleware) Authentication or authorization mechanisms Database migration tools These concerns are best handled by your chosen framework or dedicated libraries, allowing Backslash to remain framework-agnostic and focused on its core purpose.\n","date":"0001-01-01","id":7,"permalink":"/docs/getting-started/architecture-overview/","summary":"\u003cp\u003eBackslash is designed around the core principles of event sourcing and CQRS. Its modular, lightweight, and\nframework-agnostic architecture allows for gradual adoption or integration into existing systems.\u003c/p\u003e","tags":[],"title":"Architecture Overview"},{"content":"Commands represent intentions to change the system\u0026rsquo;s state. Unlike events that describe what happened, commands express what should happen. They carry the information needed to make a business decision and are the entry points for all write operations.\nCommands are dispatched through a Dispatcher that routes them to their corresponding handlers. Command handlers coordinate whatever operations are necessary to fulfill the command\u0026rsquo;s intent.\nDefining commands Commands are simple data transfer objects with no behavior. They don\u0026rsquo;t need to implement any interface; they\u0026rsquo;re just plain PHP classes that carry data:\nreadonly class RegisterStudentCommand { public function __construct( public string $studentId, public string $name, ) { } }\rMark commands as readonly to emphasize their immutability. Commands should never change once created.\nNaming commands Command names should clearly express intent using imperative verbs:\n// Good: clear intent RegisterStudentCommand ChangeCourseCapacityCommand SubscribeStudentToCourseCommand UnsubscribeStudentFromCourseCommand // Avoid: unclear or passive names StudentCommand CourseCapacityCommand StudentCourseCommand\rCreating command handlers Command handlers implement HandlerInterface:\ninterface HandlerInterface { public function handle(object $command): void; }\rUse HandleCommandTrait to automatically route commands to methods based on class names:\nuse Backslash\\CommandDispatcher\\HandlerInterface; use Backslash\\CommandDispatcher\\HandleCommandTrait; class StudentCommandHandler implements HandlerInterface { use HandleCommandTrait; public function __construct( private RepositoryInterface $repository, ) { } protected function handleRegisterStudentCommand( RegisterStudentCommand $command, ): void { $query = StudentRegistrationModel::buildQuery($command-\u0026gt;studentId); $model = $this-\u0026gt;repository-\u0026gt;loadModel( StudentRegistrationModel::class, $query ); $model-\u0026gt;register($command-\u0026gt;studentId, $command-\u0026gt;name); $this-\u0026gt;repository-\u0026gt;storeChanges($model); } }\rThe trait looks for methods named by prefixing handle to the command\u0026rsquo;s short class name.\nThe command handler workflow A common pattern for command handlers working with models follows these steps:\nBuild the query to load relevant events Load the model from the Repository Execute business logic by calling decision methods Persist changes back to the Repository protected function handleChangeCourseCapacityCommand( ChangeCourseCapacityCommand $command, ): void { // 1. Build query $query = CourseCapacityModel::buildQuery($command-\u0026gt;courseId); // 2. Load model $model = $this-\u0026gt;repository-\u0026gt;loadModel( CourseCapacityModel::class, $query ); // 3. Execute business logic $model-\u0026gt;change($command-\u0026gt;capacity); // 4. Persist changes $this-\u0026gt;repository-\u0026gt;storeChanges($model); }\rThe Repository handles event replay, concurrency checks, event persistence, and event publishing. Command handlers coordinate operations.\nHowever, command handlers are not limited to working with models. They can perform any operation needed to fulfill the command\u0026rsquo;s intent. For example, a handler might call external services, send notifications, or perform system operations:\nclass SystemCommandHandler implements HandlerInterface { use HandleCommandTrait; public function __construct( private MailerInterface $mailer, private LoggerInterface $logger, ) { } protected function handleSendWelcomeEmailCommand( SendWelcomeEmailCommand $command, ): void { $this-\u0026gt;mailer-\u0026gt;send( to: $command-\u0026gt;email, subject: \u0026#39;Welcome!\u0026#39;, body: $this-\u0026gt;buildWelcomeMessage($command-\u0026gt;name) ); $this-\u0026gt;logger-\u0026gt;info(\u0026#39;Welcome email sent\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $command-\u0026gt;email, ]); } private function buildWelcomeMessage(string $name): string { return \u0026#34;Hello {$name}, welcome to our platform!\u0026#34;; } }\rThe key is that handlers encapsulate the logic needed to execute a command, whether that involves models, external services, or any other operations.\nRegistering handlers Register command handlers with the Dispatcher during application bootstrap:\n$dispatcher-\u0026gt;registerHandler( RegisterStudentCommand::class, new HandlerProxy(fn() =\u0026gt; $container-\u0026gt;get(StudentCommandHandler::class)) );\rAdding middleware Middleware wraps command dispatch to add cross-cutting concerns like logging, validation, or transactions:\n$dispatcher = new Dispatcher(); // Add transaction management $dispatcher-\u0026gt;addMiddleware( new ProjectionStoreTransactionCommandDispatcherMiddleware($projectionStore) );\rMiddleware executes in LIFO order. The last registered middleware wraps all previous ones.\nBest practices Keep commands simple. Commands should be data containers with no logic. All validation and business rules belong in models.\nUse descriptive command names. Command names should clearly express intent using action verbs.\nGroup related commands. Commands that share dependencies or operate on related concepts can share a handler.\nHandle each command once. Each command class should map to exactly one handler method.\nLet exceptions propagate. Domain exceptions should propagate to the dispatcher where middleware can handle them consistently.\nKeep handlers focused. Command handlers should coordinate operations, not implement business logic. Business rules belong in models.\nMake commands serializable. Commands should only contain scalar types and arrays so they can be serialized for queuing or logging.\n","date":"0001-01-01","id":8,"permalink":"/docs/cqrs/dispatching-commands/","summary":"\u003cp\u003eCommands represent intentions to change the system\u0026rsquo;s state. Unlike events that describe what happened, commands express\nwhat should happen. They carry the information needed to make a business decision and are the entry points for all write\noperations.\u003c/p\u003e","tags":[],"title":"Dispatching Commands"},{"content":"Event stream enrichment allows applications to inject metadata into events as they flow through the system. This metadata provides contextual information like the current tenant, authenticated user, or correlation keys.\nEnrichment occurs after the model records events but before they are persisted to EventStore or published to EventBus.\nUnderstanding stream enrichment An application should have a single implementation of StreamEnricherInterface:\ninterface StreamEnricherInterface { public function enrich(RecordedEventStream $stream): RecordedEventStream; }\rUsing a single enricher avoids metadata conflicts, ensures predictable execution order, and simplifies configuration. The enricher\u0026rsquo;s role is to inject metadata into events. This implementation can receive services as dependencies when contextual information is needed. Common metadata includes:\nCurrent tenant in multi-tenant applications Authenticated user information Correlation IDs for request tracing Execution context (batch job, API request, CLI command) Creating a stream enricher Here\u0026rsquo;s an example enricher that adds tenant and user information:\nuse Backslash\\StreamEnricher\\StreamEnricherInterface; use Backslash\\Event\\RecordedEventStream; use Backslash\\Event\\RecordedEvent; class StreamEnricher implements StreamEnricherInterface { public function __construct( private TenantService $tenantService, private AuthService $authService, private CorrelationIdService $correlationService, ) { } public function enrich(RecordedEventStream $stream): RecordedEventStream { $enrichedStream = new RecordedEventStream(); foreach ($stream-\u0026gt;getRecordedEvents() as $recordedEvent) { $metadata = $recordedEvent-\u0026gt;getMetadata(); // Add tenant information if ($this-\u0026gt;tenantService-\u0026gt;hasTenant()) { $metadata = $metadata-\u0026gt;with(\u0026#39;tenant_id\u0026#39;, $this-\u0026gt;tenantService-\u0026gt;getCurrentTenantId()); } // Add authenticated user if ($this-\u0026gt;authService-\u0026gt;isAuthenticated()) { $metadata = $metadata-\u0026gt;with(\u0026#39;user_id\u0026#39;, $this-\u0026gt;authService-\u0026gt;getCurrentUserId()); } // Add correlation ID for tracing $metadata = $metadata-\u0026gt;with(\u0026#39;correlation_id\u0026#39;, $this-\u0026gt;correlationService-\u0026gt;get()); $enrichedStream = $enrichedStream-\u0026gt;withRecordedEvents( $recordedEvent-\u0026gt;withMetadata($metadata) ); } return $enrichedStream; } }\rThis enricher receives services as dependencies and uses them to inject relevant metadata into every event.\nRegistering the enricher Backslash provides two middleware for stream enrichment, as introduced in section 14:\nStreamEnricherEventStoreMiddleware: Enriches events before persisting to EventStore StreamEnricherEventBusMiddleware: Enriches events before publishing to EventBus Register both middleware during application bootstrap to ensure consistent metadata in storage and when published:\nuse Backslash\\StreamEnricher\\StreamEnricherEventStoreMiddleware; use Backslash\\StreamEnricher\\StreamEnricherEventBusMiddleware; $enricher = new StreamEnricher($tenantService, $authService, $correlationService); // Enrich before persisting $eventStore-\u0026gt;addMiddleware(new StreamEnricherEventStoreMiddleware($enricher)); // Enrich before publishing $eventBus-\u0026gt;addMiddleware(new StreamEnricherEventBusMiddleware($enricher));\rUsing enriched metadata Enriched metadata becomes available throughout the application. Event handlers can access this metadata to make decisions or add context:\nclass NotificationHandler implements EventHandlerInterface { use EventHandlerTrait; private function handleStudentSubscribedToCourseEvent( StudentSubscribedToCourseEvent $event, RecordedEvent $recordedEvent, ): void { $metadata = $recordedEvent-\u0026gt;getMetadata(); // Access enriched tenant information $tenantId = $metadata-\u0026gt;get(\u0026#39;tenant_id\u0026#39;); // Access correlation ID for tracing $correlationId = $metadata-\u0026gt;get(\u0026#39;correlation_id\u0026#39;); $this-\u0026gt;logger-\u0026gt;info(\u0026#39;Student subscribed\u0026#39;, [ \u0026#39;tenant_id\u0026#39; =\u0026gt; $tenantId, \u0026#39;correlation_id\u0026#39; =\u0026gt; $correlationId, \u0026#39;student_id\u0026#39; =\u0026gt; $event-\u0026gt;studentId, ]); // Send notification with tenant context $this-\u0026gt;notifier-\u0026gt;send($event-\u0026gt;studentId, $tenantId); } }\rQueries can also filter events based on enriched metadata:\nuse Backslash\\EventStore\\Query\\Metadata; // Load only events for specific tenant $query = EventClass::is(CourseDefinedEvent::class) -\u0026gt;and(Metadata::is(\u0026#39;tenant_id\u0026#39;, $currentTenantId));\rBest practices Use one enricher per application. Create a single StreamEnricherInterface implementation that handles all metadata injection; avoid multiple enrichers with overlapping concerns.\nInject services as dependencies when needed. If the enricher requires contextual information, pass services like authentication, tenant management, or correlation tracking as constructor dependencies rather than accessing global state.\nRegister both middleware. Always register StreamEnricherEventStoreMiddleware and StreamEnricherEventBusMiddleware to ensure metadata consistency between persisted events and published events.\nEnrich metadata, not payloads. Event payloads should contain domain information; metadata is for technical concerns like tenant context, user information, or correlation IDs.\nAvoid sensitive data. Metadata is stored and potentially logged; avoid adding passwords, tokens, or other sensitive information unless absolutely required.\nKeep enrichment fast. Enrichment runs for every event; avoid expensive operations like database queries or external API calls.\n","date":"0001-01-01","id":9,"permalink":"/docs/customization/enriching-event-streams/","summary":"\u003cp\u003eEvent stream enrichment allows applications to inject metadata into events as they flow through the system. This\nmetadata provides contextual information like the current tenant, authenticated user, or correlation keys.\u003c/p\u003e","tags":[],"title":"Enriching Event Streams"},{"content":"","date":"0001-01-01","id":10,"permalink":"/docs/event-sourcing/","summary":"","tags":[],"title":"Event Sourcing"},{"content":"The boot process registers commands with the dispatcher and events with the event bus, establishing the routing between messages and their handlers. This registration happens after service definitions are loaded but before the application handles any requests or executes commands.\nUnderstanding the boot process The boot process consists of:\nRegistering commands with command handlers in the dispatcher Registering events with projectors in the event bus Registering events with processors in the event bus (skipped during projection rebuilds) This registration should happen once during application startup.\nCommand and event handler mappings are defined statically (typically as class constants) and used during the boot process to perform the actual registration.\nDefining command handler mappings Define command handler mappings using an associative array:\nprivate const COMMAND_HANDLERS = [ CourseCommandHandler::class =\u0026gt; [ ChangeCourseCapacityCommand::class, DefineCourseCommand::class, ], StudentCommandHandler::class =\u0026gt; [ RegisterStudentCommand::class, ], SubscriptionCommandHandler::class =\u0026gt; [ SubscribeStudentToCourseCommand::class, UnsubscribeStudentFromCourseCommand::class, ], ];\rEach command handler class maps to an array of command classes it handles.\nAlternatively, implement a static getHandledCommands() method on each command handler to make the mapping explicit:\nclass CourseCommandHandler { public static function getHandledCommands(): array { return [ ChangeCourseCapacityCommand::class, DefineCourseCommand::class, ]; } // Handler methods... }\rThis approach makes each handler explicitly declare what it handles, eliminating the need for a centralized mapping array.\nDefining event handler mappings Define event handler mappings, separating projectors from processors:\nprivate const PROJECTORS = [ CourseListProjector::class =\u0026gt; [ CourseDefinedEvent::class, CourseCapacityChangedEvent::class, StudentSubscribedToCourseEvent::class, StudentUnsubscribedFromCourseEvent::class, ], StudentListProjector::class =\u0026gt; [ CourseDefinedEvent::class, StudentRegisteredEvent::class, StudentSubscribedToCourseEvent::class, StudentUnsubscribedFromCourseEvent::class, ], ]; private const PROCESSORS = [ NotificationProcessor::class =\u0026gt; [ StudentSubscribedToCourseEvent::class, StudentUnsubscribedFromCourseEvent::class, ], EmailProcessor::class =\u0026gt; [ StudentRegisteredEvent::class, ], ];\rMaintain separate lists for projectors and processors to enable selective registration.\nAlternatively, implement a static getSubscribedEvents() method on each event handler to make the mapping explicit:\nclass CourseListProjector { public static function getSubscribedEvents(): array { return [ CourseDefinedEvent::class, CourseCapacityChangedEvent::class, StudentSubscribedToCourseEvent::class, StudentUnsubscribedFromCourseEvent::class, ]; } // Handler methods... }\rThis approach makes each handler explicitly declare what events it subscribes to, eliminating the need for centralized mapping arrays.\nUsing handler discovery When using the static method approach (getHandledCommands() and getSubscribedEvents()), the boot process can discover and register handlers automatically:\n// List of all command handler classes $commandHandlerClasses = [ CourseCommandHandler::class, StudentCommandHandler::class, SubscriptionCommandHandler::class, ]; // Register command handlers using discovery foreach ($commandHandlerClasses as $handlerClass) { $commands = $handlerClass::getHandledCommands(); foreach ($commands as $commandClass) { $container-\u0026gt;get(DispatcherInterface::class)-\u0026gt;registerHandler( $commandClass, new HandlerProxy(fn () =\u0026gt; $container-\u0026gt;get($handlerClass)) ); } } // List of all event handler classes $projectorClasses = [ CourseListProjector::class, StudentListProjector::class, ]; $processorClasses = [ NotificationProcessor::class, EmailProcessor::class, ]; // Register projectors using discovery foreach ($projectorClasses as $projectorClass) { $events = $projectorClass::getSubscribedEvents(); foreach ($events as $eventClass) { $container-\u0026gt;get(EventBusInterface::class)-\u0026gt;subscribe( $eventClass, new EventHandlerProxy(fn () =\u0026gt; $container-\u0026gt;get($projectorClass)) ); } } // Register processors (skip during projection rebuilds) $isRebuildMode = (bool) getenv(\u0026#39;REBUILD_PROJECTIONS\u0026#39;); if (!$isRebuildMode) { foreach ($processorClasses as $processorClass) { $events = $processorClass::getSubscribedEvents(); foreach ($events as $eventClass) { $container-\u0026gt;get(EventBusInterface::class)-\u0026gt;subscribe( $eventClass, new EventHandlerProxy(fn () =\u0026gt; $container-\u0026gt;get($processorClass)) ); } } }\rThis discovery approach requires maintaining only a list of handler classes rather than complete mappings, reducing duplication since each handler already declares what it handles.\nUsing HandlerProxy and EventHandlerProxy Backslash provides proxy classes that defer handler instantiation:\nHandlerProxy for command handlers:\nuse Backslash\\CommandDispatcher\\HandlerProxy; $dispatcher-\u0026gt;registerHandler( DefineCourseCommand::class, new HandlerProxy(fn () =\u0026gt; $container-\u0026gt;get(CourseCommandHandler::class)) );\rEventHandlerProxy for event handlers:\nuse Backslash\\EventBus\\EventHandlerProxy; $eventBus-\u0026gt;subscribe( CourseDefinedEvent::class, new EventHandlerProxy(fn () =\u0026gt; $container-\u0026gt;get(CourseListProjector::class)) );\rThese proxies defer handler instantiation until they\u0026rsquo;re actually needed.\nRegistering command handlers The boot process registers command handlers with the dispatcher:\nuse Backslash\\CommandDispatcher\\DispatcherInterface; use Backslash\\CommandDispatcher\\HandlerProxy; /** @var DispatcherInterface $dispatcher */ $dispatcher = $container-\u0026gt;get(DispatcherInterface::class); foreach (COMMAND_HANDLERS as $handlerClass =\u0026gt; $commandClasses) { foreach ($commandClasses as $commandClass) { $dispatcher-\u0026gt;registerHandler( $commandClass, new HandlerProxy(fn () =\u0026gt; $container-\u0026gt;get($handlerClass)) ); } }\rThis registration happens during boot, before dispatching any commands.\nRegistering event handlers The boot process registers event handlers with the event bus:\nuse Backslash\\EventBus\\EventBusInterface; use Backslash\\EventBus\\EventHandlerProxy; /** @var EventBusInterface $eventBus */ $eventBus = $container-\u0026gt;get(EventBusInterface::class); // Register projectors foreach (PROJECTORS as $projectorClass =\u0026gt; $eventClasses) { foreach ($eventClasses as $eventClass) { $eventBus-\u0026gt;subscribe( $eventClass, new EventHandlerProxy(fn () =\u0026gt; $container-\u0026gt;get($projectorClass)) ); } } // Register processors (skip during projection rebuilds) $isRebuildMode = (bool) getenv(\u0026#39;REBUILD_PROJECTIONS\u0026#39;); if (!$isRebuildMode) { foreach (PROCESSORS as $processorClass =\u0026gt; $eventClasses) { foreach ($eventClasses as $eventClass) { $eventBus-\u0026gt;subscribe( $eventClass, new EventHandlerProxy(fn () =\u0026gt; $container-\u0026gt;get($processorClass)) ); } } }\rThe application determines whether it\u0026rsquo;s in rebuild mode (typically using an environment variable like REBUILD_PROJECTIONS). During projection rebuilds, processors are not registered to avoid triggering side effects like sending duplicate notifications or making external API calls.\n","date":"0001-01-01","id":11,"permalink":"/docs/application-setup/registering-handlers/","summary":"\u003cp\u003eThe boot process registers commands with the dispatcher and events with the event bus, establishing the routing between\nmessages and their handlers. This registration happens after service definitions are loaded but before the application\nhandles any requests or executes commands.\u003c/p\u003e","tags":[],"title":"Registering Handlers"},{"content":"The Scenario component provides a fluent, behavior-driven interface for testing event-sourced applications. It uses the Play class to define test scenarios in a readable, expressive Given-When-Then manner.\nUnderstanding scenarios Scenarios test the complete flow from command dispatch through event publication to projection updates. They verify that commands produce the expected events and that projections are updated correctly.\nThe Scenario class was instantiated in your base test case (section 12). Each test creates Play instances and executes them via scenario-\u0026gt;play().\nA scenario can execute multiple plays sequentially, similar to acts in a theater piece. Each play represents a distinct phase of the test, and subsequent plays see the effects of previous plays.\nWriting basic scenarios with Play Create test scenarios using the Play class with the Given-When-Then pattern. A Play defines:\nInitial state (Given) via given() - accepts events or commands Actions (When) via when() - accepts commands or closures Assertions (Then) via then() - automatic parameter routing based on type hints Expected exceptions via thenExpectException() or thenExpectExceptionMessage() The order of these methods matters: setup methods should come first, followed by actions, then assertions.\nHere\u0026rsquo;s a basic example:\n#[Test] public function it_publishes_event_when_registering_student(): void { $this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;when(new RegisterStudentCommand(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;)) -\u0026gt;then(fn (PublishedEvents $events) =\u0026gt; $this-\u0026gt;assertPublishedEventsContainExactly([ StudentRegisteredEvent::class =\u0026gt; 1, ], $events) ) ); }\rSetting up initial state with given() Use given() to establish the initial state before executing your test action. The given() method accepts both events and commands, making it flexible for different testing scenarios.\nGiven with commands Commands passed to given() are dispatched before the main test action, setting up the required initial state:\n$this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;given( new DefineCourseCommand(\u0026#39;123\u0026#39;, \u0026#39;PHP Basics\u0026#39;, 10), new RegisterStudentCommand(\u0026#39;1\u0026#39;, \u0026#39;Alice\u0026#39;) ) -\u0026gt;when(new SubscribeStudentToCourseCommand(\u0026#39;1\u0026#39;, \u0026#39;123\u0026#39;)) -\u0026gt;then(fn (PublishedEvents $events) =\u0026gt; $this-\u0026gt;assertPublishedEventsContain( StudentSubscribedToCourseEvent::class, $events ) ) );\rGiven with events You can also use given() to directly set up state with events, which is useful when you want to bypass command validation or set up specific event sequences:\n$this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;given( new CourseDefinedEvent(\u0026#39;123\u0026#39;, \u0026#39;PHP Basics\u0026#39;, 10) ) -\u0026gt;when(new ChangeCourseCapacityCommand(\u0026#39;123\u0026#39;, 20)) -\u0026gt;then(fn (PublishedEvents $events) =\u0026gt; $this-\u0026gt;assertPublishedEventsContain( CourseCapacityChangedEvent::class, $events ) ) );\rThe given() method automatically wraps raw events in RecordedEvent instances with appropriate metadata.\nExecuting actions with when() The when() method is the heart of your test - it defines what action you\u0026rsquo;re testing. It accepts both commands and closures, giving you flexibility in how you trigger the behavior under test.\nWhen with commands Pass commands to when() to execute them through the normal command handling flow:\n$this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;when( new RegisterStudentCommand(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;), new RegisterStudentCommand(\u0026#39;2\u0026#39;, \u0026#39;Alice\u0026#39;) ) -\u0026gt;then(fn (PublishedEvents $events) =\u0026gt; $this-\u0026gt;assertPublishedEventsCount(2, $events) ) );\rWhen with closures Use closures with when() to execute custom logic during the test. The closure can receive a RepositoryInterface parameter for direct model manipulation:\n$this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;given(new StudentRegisteredEvent(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;)) -\u0026gt;when(function (RepositoryInterface $repo): void { $model = $repo-\u0026gt;loadModel( Student::class, Identifier::is(\u0026#39;studentId\u0026#39;, \u0026#39;1\u0026#39;) ); $model-\u0026gt;changeName(\u0026#39;Jane\u0026#39;); $repo-\u0026gt;storeChanges($model); }) -\u0026gt;then(fn (PublishedEvents $events) =\u0026gt; $this-\u0026gt;assertNotEmpty($events-\u0026gt;getAllOf(StudentNameChangedEvent::class)) ) );\rClosures can also be used for other purposes like defining constants or setting up test data:\n$this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;given(new DefineCourseCommand(\u0026#39;123\u0026#39;, \u0026#39;PHP Basics\u0026#39;, 10)) -\u0026gt;when(function (): void { define(\u0026#39;MY_CONSTANT\u0026#39;, \u0026#39;some-value\u0026#39;); }) -\u0026gt;when(new ChangeCourseCapacityCommand(\u0026#39;123\u0026#39;, 20)) -\u0026gt;then(fn () =\u0026gt; $this-\u0026gt;assertTrue(defined(\u0026#39;MY_CONSTANT\u0026#39;))) );\rMultiple when() calls execute in the order they\u0026rsquo;re defined.\nWriting assertions with then() The then() method is where you verify the results of your test. It supports automatic parameter routing based on type hints, allowing you to assert on events, projections, or use the repository directly.\nAsserting on published events Use then() with a PublishedEvents parameter to verify which events were published:\n$this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;when(new RegisterStudentCommand(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;)) -\u0026gt;then(fn (PublishedEvents $events) =\u0026gt; $this-\u0026gt;assertPublishedEventsContainExactly([ StudentRegisteredEvent::class =\u0026gt; 1, ], $events) ) -\u0026gt;then(function (PublishedEvents $events) { // You can also use a full closure for more complex assertions $this-\u0026gt;assertPublishedEventsContain( StudentRegisteredEvent::class, $events ); $this-\u0026gt;assertPublishedEventsCount(1, $events); }) );\rThe PublishedEvents parameter contains all events published during the when() phase only. Events from the given() phase are not included in this collection.\nAsserting on updated projections Use then() with an UpdatedProjections parameter to verify which projections were updated:\n$this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;given( new DefineCourseCommand(\u0026#39;123\u0026#39;, \u0026#39;PHP Basics\u0026#39;, 10), new RegisterStudentCommand(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;) ) -\u0026gt;when(new SubscribeStudentToCourseCommand(\u0026#39;1\u0026#39;, \u0026#39;123\u0026#39;)) -\u0026gt;then(fn (UpdatedProjections $projections) =\u0026gt; $this-\u0026gt;assertUpdatedProjectionsContain( StudentListProjection::class, $projections ) ) -\u0026gt;then(function (UpdatedProjections $projections) { // Access updated projections directly $studentList = $projections-\u0026gt;getAllOf(StudentListProjection::class)[0]; $this-\u0026gt;assertStringContainsString(\u0026#39;John (PHP Basics)\u0026#39;, (string) $studentList); }) );\rThe UpdatedProjections parameter contains projections modified during the when() phase only. Projections updated during given() are not included.\nUse getAllOf() to retrieve all updated projections of a specific type. This returns an array because multiple projection instances of the same class can be updated in a single play (for example, updating both CourseProjection for course-123 and CourseProjection for course-456).\nCustom assertions with repository Use then() with a RepositoryInterface parameter to load and verify model state directly:\n$this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;when(new RegisterStudentCommand(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;)) -\u0026gt;then(function (RepositoryInterface $repo) { $student = $repo-\u0026gt;loadModel( Student::class, Identifier::is(\u0026#39;studentId\u0026#39;, \u0026#39;1\u0026#39;) ); $this-\u0026gt;assertEquals(\u0026#39;John\u0026#39;, $student-\u0026gt;getName()); }) );\rCustom assertions without parameters Use then() with no parameters for arbitrary assertion logic:\n$this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;when(new RegisterStudentCommand(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;)) -\u0026gt;then(function () { $projection = $this-\u0026gt;projectionStore-\u0026gt;find( StudentListProjection::ID, StudentListProjection::class ); $this-\u0026gt;assertStringContainsString(\u0026#39;John\u0026#39;, (string) $projection); }) );\rAll then() callbacks execute after the when() phase completes, and you can chain multiple then() calls with different parameter types.\nTesting business rule violations Test that commands throw exceptions using thenExpectException():\nuse PHPUnit\\Framework\\Attributes\\DoesNotPerformAssertions; use PHPUnit\\Framework\\Attributes\\Test; #[Test] #[DoesNotPerformAssertions] public function it_prevents_subscription_to_full_course(): void { $this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;given( new DefineCourseCommand(\u0026#39;123\u0026#39;, \u0026#39;PHP Basics\u0026#39;, 1), new RegisterStudentCommand(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;), new RegisterStudentCommand(\u0026#39;2\u0026#39;, \u0026#39;Alice\u0026#39;), new SubscribeStudentToCourseCommand(\u0026#39;1\u0026#39;, \u0026#39;123\u0026#39;) ) -\u0026gt;when(new SubscribeStudentToCourseCommand(\u0026#39;2\u0026#39;, \u0026#39;123\u0026#39;)) -\u0026gt;thenExpectException(CourseAtFullCapacityException::class) ); }\rWhen thenExpectException() is used, the test passes only if the expected exception is thrown during the when() phase. Use the #[DoesNotPerformAssertions] PHPUnit attribute to avoid warnings about tests without assertions.\nTest for specific exception messages using thenExpectExceptionMessage():\n#[Test] #[DoesNotPerformAssertions] public function it_shows_helpful_error_message_for_full_course(): void { $this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;given( new DefineCourseCommand(\u0026#39;123\u0026#39;, \u0026#39;PHP Basics\u0026#39;, 1), new RegisterStudentCommand(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;), new RegisterStudentCommand(\u0026#39;2\u0026#39;, \u0026#39;Alice\u0026#39;), new SubscribeStudentToCourseCommand(\u0026#39;1\u0026#39;, \u0026#39;123\u0026#39;) ) -\u0026gt;when(new SubscribeStudentToCourseCommand(\u0026#39;2\u0026#39;, \u0026#39;123\u0026#39;)) -\u0026gt;thenExpectExceptionMessage(\u0026#39;Course is at full capacity\u0026#39;) ); }\rChaining multiple plays A scenario can execute multiple plays sequentially, like acts in a theater piece. Each play represents a distinct phase of the test:\n#[Test] public function it_handles_complete_subscription_lifecycle(): void { $subscribe = new Play() -\u0026gt;given( new DefineCourseCommand(\u0026#39;123\u0026#39;, \u0026#39;PHP Basics\u0026#39;, 10), new RegisterStudentCommand(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;) ) -\u0026gt;when(new SubscribeStudentToCourseCommand(\u0026#39;1\u0026#39;, \u0026#39;123\u0026#39;)) -\u0026gt;then(fn (PublishedEvents $events) =\u0026gt; $this-\u0026gt;assertPublishedEventsContain( StudentSubscribedToCourseEvent::class, $events ) ); $unsubscribe = new Play() -\u0026gt;when(new UnsubscribeStudentFromCourseCommand(\u0026#39;1\u0026#39;, \u0026#39;123\u0026#39;)) -\u0026gt;then(fn (PublishedEvents $events) =\u0026gt; $this-\u0026gt;assertPublishedEventsContain( StudentUnsubscribedFromCourseEvent::class, $events ) ); $this-\u0026gt;scenario-\u0026gt;play($subscribe, $unsubscribe); }\rEach play executes in order, like acts in a theater piece. The second play (unsubscribe) sees all the effects of the first play (subscribe), including published events and updated projections. This allows you to test multi-step workflows while keeping each phase clearly defined and testable.\nComplete scenario example Here\u0026rsquo;s a comprehensive example demonstrating all Play features and assertions:\nuse PHPUnit\\Framework\\Attributes\\DoesNotPerformAssertions; use PHPUnit\\Framework\\Attributes\\Test; #[Test] public function it_manages_complete_subscription_workflow(): void { $studentId = \u0026#39;1\u0026#39;; $courseId = \u0026#39;2\u0026#39;; $subscribe = new Play() -\u0026gt;given( new CourseDefinedEvent($courseId, \u0026#39;Maths\u0026#39;, 30), new RegisterStudentCommand($studentId, \u0026#39;John\u0026#39;), ) -\u0026gt;when(new SubscribeStudentToCourseCommand($studentId, $courseId)) -\u0026gt;when(function (): void { define(\u0026#39;MY_CONSTANT\u0026#39;, \u0026#39;some-value\u0026#39;); }) -\u0026gt;then(function (PublishedEvents $events) use ($studentId, $courseId): void { $this-\u0026gt;assertPublishedEventsCount(1, $events); $this-\u0026gt;assertPublishedEventsContainOnly(StudentSubscribedToCourseEvent::class, $events); $this-\u0026gt;assertPublishedEventsDoNotContain(StudentUnsubscribedFromCourseEvent::class, $events); /** @var StudentSubscribedToCourseEvent $event */ $event = $events-\u0026gt;getAllOf(StudentSubscribedToCourseEvent::class)[0]-\u0026gt;getEvent(); $this-\u0026gt;assertEquals($studentId, $event-\u0026gt;studentId); $this-\u0026gt;assertEquals($courseId, $event-\u0026gt;courseId); }) -\u0026gt;then(fn (UpdatedProjections $projections) =\u0026gt; $this-\u0026gt;assertUpdatedProjectionsContainExactly([ CourseListProjection::class =\u0026gt; 1, StudentListProjection::class =\u0026gt; 1, ], $projections) ) -\u0026gt;then(fn () =\u0026gt; $this-\u0026gt;assertTrue(defined(\u0026#39;MY_CONSTANT\u0026#39;))); $unsubscribe = new Play() -\u0026gt;when(new UnsubscribeStudentFromCourseCommand($studentId, $courseId)) -\u0026gt;then(fn (PublishedEvents $events) =\u0026gt; $this-\u0026gt;assertPublishedEventsContainExactly([ StudentUnsubscribedFromCourseEvent::class =\u0026gt; 1, ], $events) ); $oops = new Play() -\u0026gt;when(new UnsubscribeStudentFromCourseCommand($studentId, $courseId)) -\u0026gt;thenExpectException(StudentNotSubscribedToCourseException::class); $this-\u0026gt;scenario-\u0026gt;play($subscribe, $unsubscribe, $oops); }\rThis example demonstrates:\ngiven(): Setting up state with both events and commands when(): Executing commands and arbitrary actions then() with PublishedEvents: Comprehensive event assertions including count, type, and content verification then() with UpdatedProjections: Verifying projection updates with exact counts then() with no parameters: Custom assertion logic thenExpectException(): Testing business rule violations Multiple plays: Three sequential plays representing different phases of the workflow Optimizing test performance with event publishing modes By default, events generated in both given() and when() phases are published to the event bus, which triggers projection updates. This ensures backwards compatibility and predictable behavior. However, for better performance, you can optimize when events are published.\nAutomatic projection detection (DETECT mode) The EventPublishingMode::DETECT mode automatically analyzes your test to determine if projections are needed:\nEvents in given() are NOT published (test setup only) Events in when() are published only if your assertions reference UpdatedProjections More efficient as projections are only updated when actually needed for assertions Enable this mode at the scenario level:\nuse Backslash\\Scenario\\EventPublishingMode; class StudentTest extends TestCase { private Scenario $scenario; protected function setUp(): void { $this-\u0026gt;scenario = new Scenario(); $this-\u0026gt;scenario-\u0026gt;setEventPublishingMode(EventPublishingMode::DETECT); } }\rWith DETECT mode enabled, this test will NOT publish events during given(), and WILL publish during when() because it asserts on projections:\n#[Test] public function it_updates_student_projection(): void { $this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;given(new StudentRegisteredEvent(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;)) // NOT published -\u0026gt;when(function (RepositoryInterface $repo): void { $student = $repo-\u0026gt;loadModel(Student::class, Identifier::is(\u0026#39;studentId\u0026#39;, \u0026#39;1\u0026#39;)); $student-\u0026gt;changeName(\u0026#39;Jane\u0026#39;); $repo-\u0026gt;storeChanges($student); }) // WILL be published because of UpdatedProjections below -\u0026gt;then(fn (UpdatedProjections $projections) =\u0026gt; $this-\u0026gt;assertUpdatedProjectionsContain(StudentProjection::class, $projections) ) ); }\rChanging modes for specific tests You can change the publishing mode at any time by calling setEventPublishingMode() on the scenario:\n#[Test] public function it_always_publishes_events(): void { // Force event publishing for this specific test $this-\u0026gt;scenario-\u0026gt;setEventPublishingMode(EventPublishingMode::ALWAYS); $this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;given(new StudentRegisteredEvent(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;)) -\u0026gt;when(new ChangeNameCommand(\u0026#39;1\u0026#39;, \u0026#39;Jane\u0026#39;)) -\u0026gt;then(fn (PublishedEvents $events) =\u0026gt; ...) ); } #[Test] public function it_never_publishes_events(): void { // Prevent all event publishing for this test $this-\u0026gt;scenario-\u0026gt;setEventPublishingMode(EventPublishingMode::NEVER); $this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;given(new StudentRegisteredEvent(\u0026#39;1\u0026#39;, \u0026#39;John\u0026#39;)) -\u0026gt;when(new ChangeNameCommand(\u0026#39;1\u0026#39;, \u0026#39;Jane\u0026#39;)) -\u0026gt;then(fn (PublishedEvents $events) =\u0026gt; ...) ); }\rNote that the mode applies to all plays executed after it\u0026rsquo;s set, until you change it again.\nAvailable modes The EventPublishingMode enum provides three modes:\nALWAYS (default): Events are published in both given() and when() phases, ensuring projections are always updated. Guarantees backwards compatibility. DETECT: Automatically determines if projections are needed by analyzing your then() assertions. Events in given() are not published; events in when() are published only if needed. NEVER: Events are never published to the event bus. Use this for pure event store testing without any projection updates. Choose DETECT mode for better test performance when you don\u0026rsquo;t need projections updated during setup, or stick with ALWAYS for simpler, more predictable behavior.\nUsing AssertionsTrait The AssertionsTrait (included in your base test case) provides helpful assertions for working with scenarios:\nEvent assertions:\n// Assert an event was published $this-\u0026gt;assertPublishedEventsContain(CourseDefinedEvent::class, $events); // Assert only a specific event type was published $this-\u0026gt;assertPublishedEventsContainOnly(CourseDefinedEvent::class, $events); // Assert exact count of specific events $this-\u0026gt;assertPublishedEventsContainExactly([ CourseDefinedEvent::class =\u0026gt; 1, CourseCapacityChangedEvent::class =\u0026gt; 2, ], $events); // Assert total event count $this-\u0026gt;assertPublishedEventsCount(3, $events); // Assert an event was NOT published $this-\u0026gt;assertPublishedEventsDoNotContain(CourseDeletedEvent::class, $events);\rProjection assertions:\n// Assert a projection was updated $this-\u0026gt;assertUpdatedProjectionsContain(CourseProjection::class, $projections); // Assert only a specific projection type was updated $this-\u0026gt;assertUpdatedProjectionsContainOnly(CourseProjection::class, $projections); // Assert exact count of updated projections $this-\u0026gt;assertUpdatedProjectionsContainExactly([ CourseProjection::class =\u0026gt; 1, StudentProjection::class =\u0026gt; 2, ], $projections); // Assert total projection update count $this-\u0026gt;assertUpdatedProjectionsCount(3, $projections);\rChaining Play methods All Play methods return a new instance, allowing fluent chaining in the Given-When-Then pattern:\n#[Test] public function it_changes_course_capacity_and_updates_projection(): void { $this-\u0026gt;scenario-\u0026gt;play( new Play() -\u0026gt;given(new DefineCourseCommand(\u0026#39;123\u0026#39;, \u0026#39;PHP Basics\u0026#39;, 10)) -\u0026gt;when(new ChangeCourseCapacityCommand(\u0026#39;123\u0026#39;, 20)) -\u0026gt;then(fn (PublishedEvents $events) =\u0026gt; $this-\u0026gt;assertPublishedEventsCount(1, $events) ) -\u0026gt;then(fn (UpdatedProjections $projections) =\u0026gt; $this-\u0026gt;assertUpdatedProjectionsContain(CourseProjection::class, $projections) ) -\u0026gt;then(function () { $projection = $this-\u0026gt;projectionStore-\u0026gt;find(\u0026#39;123\u0026#39;, CourseProjection::class); $this-\u0026gt;assertEquals(20, $projection-\u0026gt;getCapacity()); }) ); }\rTesting models in isolation You can test models directly without scenarios for unit-level tests:\nuse PHPUnit\\Framework\\Attributes\\Test; #[Test] public function it_enforces_course_capacity_rules(): void { $model = new CourseCapacityModel(); // Apply initial state $initialEvents = new RecordedEventStream( RecordedEvent::create( new CourseDefinedEvent(\u0026#39;course-123\u0026#39;, \u0026#39;PHP Basics\u0026#39;, 10), new Metadata(), new DateTimeImmutable() ) ); $model-\u0026gt;applyEvents($initialEvents); // Execute business logic $model-\u0026gt;change(20); // Assert recorded events $changes = $model-\u0026gt;getChanges(); $this-\u0026gt;assertCount(1, $changes); $recordedEvent = iterator_to_array($changes)[0]; $this-\u0026gt;assertInstanceOf(CourseCapacityChangedEvent::class, $recordedEvent-\u0026gt;getEvent()); }\rThis approach tests model logic in isolation without involving the command dispatcher, event bus, or projections. Use applyEvents() with a RecordedEventStream to set up the model\u0026rsquo;s initial state, just like given() with events in scenarios.\nBest practices Use the Given-When-Then pattern. Structure your tests with given() for setup, when() for actions, and then() for assertions. This makes tests readable and self-documenting.\nSet up state with given(). Use given() to establish initial state with both events and commands. This ensures a clean separation between setup and the actual test action.\nKeep scenarios focused. Each test should verify one behavior; avoid testing multiple unrelated behaviors in a single scenario. Name tests with it_ prefix describing the expected behavior (e.g., it_publishes_event_when_registering_student).\nTest both success and failure. Write scenarios for both successful operations and business rule violations using thenExpectException().\nLeverage AssertionsTrait. Use the provided assertions (assertPublishedEventsContain(), assertUpdatedProjectionsContain()) rather than writing custom assertion logic.\nChain multiple plays for workflows. When testing multi-step processes, create separate plays for each step and execute them together via scenario-\u0026gt;play($play1, $play2).\nUse #[DoesNotPerformAssertions] for exception tests. When using thenExpectException() without additional assertions, add the #[DoesNotPerformAssertions] PHPUnit attribute to avoid warnings about tests without assertions.\nUse #[Test] attribute. Prefer the modern #[Test] attribute over the test_ prefix or @test docblock annotation for marking test methods.\nOptimize with DETECT mode. For better test performance, enable EventPublishingMode::DETECT at the scenario level to avoid unnecessary projection updates during setup. Only use ALWAYS mode when you need projections updated during given().\nType-hint then() parameters. Always type-hint the parameter in then() callbacks to enable automatic routing: then(fn (PublishedEvents $events) =\u0026gt; ...) instead of then(fn ($events) =\u0026gt; ...).\n","date":"0001-01-01","id":12,"permalink":"/docs/testing/writing-test-scenarios/","summary":"\u003cp\u003eThe \u003ccode\u003eScenario\u003c/code\u003e component provides a fluent, behavior-driven interface for testing event-sourced applications. It uses\nthe \u003ccode\u003ePlay\u003c/code\u003e class to define test scenarios in a readable, expressive Given-When-Then manner.\u003c/p\u003e","tags":[],"title":"Writing Test Scenarios"},{"content":"","date":"0001-01-01","id":13,"permalink":"/docs/cqrs/","summary":"","tags":[],"title":"Commands and Projections (CQRS)"},{"content":"This guide walks you through setting up and running the Backslash demo application, helping you understand the basic workflow before building your own application.\nInstalling the demo Clone the demo repository and install dependencies:\ngit clone https://github.com/backslashphp/demo.git cd demo composer install\rRunning the demo Execute the demo script:\ncd bin php demo.php\rThe demo will automatically create the SQLite database and execute a series of commands demonstrating:\nDefining a course with capacity limits Registering students Subscribing students to courses Handling capacity constraints Updating projections in real-time Understanding the demo structure The demo follows a vertical slice architecture (VSA):\nsrc/ ├── Feature/ │ ├── {Feature name}/ │ │ ├── Command/ # Commands and handler │ │ ├── Event/ # Domain events │ │ ├── Exception/ # Business rule violations │ │ ├── Model/ # Decision-making models │ │ └── Test/ # Test scenarios │ └── Shared/ │ └── Projection/ # Shared projections └── Infrastructure/ └── Container.php # Dependency injection\rThis structure is a recommended approach that works well for event sourcing applications. However, Backslash does not impose any specific code structure; you\u0026rsquo;re free to organize your application in whatever way suits your needs.\nExploring the code As you read through this documentation, explore the demo application\u0026rsquo;s source code. Each concept explained in the following sections has a concrete implementation in the demo that you can study and experiment with.\n","date":"0001-01-01","id":14,"permalink":"/docs/getting-started/quick-start-guide/","summary":"\u003cp\u003eThis guide walks you through setting up and running the Backslash demo application, helping you understand the basic\nworkflow before building your own application.\u003c/p\u003e","tags":[],"title":"Quick Start Guide"},{"content":"Event handlers respond to events by updating projections, triggering side effects, or integrating with external systems. They subscribe to specific event types and execute synchronously when those events are published.\nCreating event handlers Event handlers implement EventHandlerInterface:\ninterface EventHandlerInterface { public function handle(RecordedEvent $recordedEvent): void; }\rUse EventHandlerTrait to automatically route events to methods based on class names:\nuse Backslash\\EventBus\\EventHandlerInterface; use Backslash\\EventBus\\EventHandlerTrait; class NotificationHandler implements EventHandlerInterface { use EventHandlerTrait; public function __construct( private MailerInterface $mailer, ) { } private function handleStudentSubscribedToCourseEvent( StudentSubscribedToCourseEvent $event, RecordedEvent $recordedEvent, ): void { $this-\u0026gt;mailer-\u0026gt;send( to: $event-\u0026gt;studentId, subject: \u0026#39;Course Subscription Confirmed\u0026#39;, body: \u0026#34;You have been subscribed to course {$event-\u0026gt;courseId}\u0026#34; ); } }\rThe trait looks for methods named by prefixing handle to the event\u0026rsquo;s short class name. It automatically extracts the domain event from the RecordedEvent wrapper and passes both to the handler method.\nHandler method signature Handler methods follow this pattern:\nprivate function handleSomeEvent( SomeEvent $event, RecordedEvent $recordedEvent, ): void { // Handler logic }\rRequirements:\nMethod must be private (or protected if creating an abstract base handler class) First parameter is the domain event with full type information Second parameter is the RecordedEvent wrapper (optional) Must return void The RecordedEvent provides access to metadata and the recording timestamp. Most handlers only need the domain event; use the second parameter when you need metadata like correlation IDs or user context:\nprivate function handleStudentSubscribedToCourseEvent( StudentSubscribedToCourseEvent $event, RecordedEvent $recordedEvent, ): void { $correlationId = $recordedEvent-\u0026gt;getMetadata()-\u0026gt;get(\u0026#39;correlation_id\u0026#39;); $this-\u0026gt;logger-\u0026gt;info(\u0026#39;Student subscribed to course\u0026#39;, [ \u0026#39;correlation_id\u0026#39; =\u0026gt; $correlationId, \u0026#39;student_id\u0026#39; =\u0026gt; $event-\u0026gt;studentId, \u0026#39;course_id\u0026#39; =\u0026gt; $event-\u0026gt;courseId, \u0026#39;timestamp\u0026#39; =\u0026gt; $recordedEvent-\u0026gt;getRecordTime()-\u0026gt;format(\u0026#39;Y-m-d H:i:s\u0026#39;), ]); }\rSubscribing handlers to events Register handlers with the EventBus during application bootstrap:\n$eventBus-\u0026gt;subscribe(CourseDefinedEvent::class, $notificationHandler); $eventBus-\u0026gt;subscribe(StudentRegisteredEvent::class, $notificationHandler);\rA single handler can respond to multiple event types, and multiple handlers can respond to the same event.\nSubscribing to all events Use subscribeAll() to register a handler that receives every published event, regardless of type:\n$eventBus-\u0026gt;subscribeAll($loggingHandler);\rThis is useful for cross-cutting concerns like global logging, audit trails, or monitoring. Handlers registered with subscribeAll() execute after all event-specific handlers, ensuring they run last in the handler chain.\nIf a handler is registered both with subscribe() for specific events and with subscribeAll(), it will only be called once per event to avoid duplicates.\nExample logging handler:\nclass EventLoggingHandler implements EventHandlerInterface { use EventHandlerTrait; public function __construct( private LoggerInterface $logger, ) { } public function handle(RecordedEvent $recordedEvent): void { $event = $recordedEvent-\u0026gt;getEvent(); $this-\u0026gt;logger-\u0026gt;info(\u0026#39;Event published\u0026#39;, [ \u0026#39;event_type\u0026#39; =\u0026gt; $event::class, \u0026#39;event_data\u0026#39; =\u0026gt; $event-\u0026gt;toArray(), \u0026#39;recorded_at\u0026#39; =\u0026gt; $recordedEvent-\u0026gt;getRecordTime()-\u0026gt;format(\u0026#39;Y-m-d H:i:s\u0026#39;), \u0026#39;metadata\u0026#39; =\u0026gt; $recordedEvent-\u0026gt;getMetadata()-\u0026gt;toArray(), ]); } }\rWhen using subscribeAll(), implement the handle() method directly instead of using the trait\u0026rsquo;s automatic routing, since you\u0026rsquo;re handling all event types rather than specific ones.\nHandling multiple event types A single handler class can respond to different events by implementing multiple handler methods:\nclass CourseNotificationHandler implements EventHandlerInterface { use EventHandlerTrait; public function __construct( private MailerInterface $mailer, private LoggerInterface $logger, ) { } private function handleCourseDefinedEvent( CourseDefinedEvent $event, RecordedEvent $recordedEvent, ): void { $this-\u0026gt;logger-\u0026gt;info(\u0026#39;New course defined\u0026#39;, [ \u0026#39;course_id\u0026#39; =\u0026gt; $event-\u0026gt;courseId, \u0026#39;name\u0026#39; =\u0026gt; $event-\u0026gt;name, ]); } private function handleCourseCapacityChangedEvent( CourseCapacityChangedEvent $event, RecordedEvent $recordedEvent, ): void { $this-\u0026gt;mailer-\u0026gt;sendToAdmins( subject: \u0026#39;Course Capacity Changed\u0026#39;, body: \u0026#34;Course {$event-\u0026gt;courseId} capacity changed from {$event-\u0026gt;previous} to {$event-\u0026gt;new}\u0026#34; ); } private function handleStudentSubscribedToCourseEvent( StudentSubscribedToCourseEvent $event, RecordedEvent $recordedEvent, ): void { $this-\u0026gt;mailer-\u0026gt;send( to: $event-\u0026gt;studentId, subject: \u0026#39;Subscription Confirmed\u0026#39;, body: \u0026#34;You are now enrolled in course {$event-\u0026gt;courseId}\u0026#34; ); } }\rRegister the handler for all relevant events:\n$handler = new CourseNotificationHandler($mailer, $logger); $eventBus-\u0026gt;subscribe(CourseDefinedEvent::class, $handler); $eventBus-\u0026gt;subscribe(CourseCapacityChangedEvent::class, $handler); $eventBus-\u0026gt;subscribe(StudentSubscribedToCourseEvent::class, $handler);\rLazy loading handlers Avoid instantiating all handlers during bootstrap using EventHandlerProxy:\nuse Backslash\\EventBus\\EventHandlerProxy; $eventBus-\u0026gt;subscribe( CourseDefinedEvent::class, new EventHandlerProxy(fn() =\u0026gt; $container-\u0026gt;get(NotificationHandler::class)) );\rThe proxy defers handler instantiation until the first relevant event is published.\nHandler execution order Multiple handlers subscribed to the same event execute synchronously in registration order:\n$eventBus-\u0026gt;subscribe(StudentSubscribedToCourseEvent::class, $courseProjector); // First $eventBus-\u0026gt;subscribe(StudentSubscribedToCourseEvent::class, $studentProjector); // Second $eventBus-\u0026gt;subscribe(StudentSubscribedToCourseEvent::class, $emailNotifier); // Third This synchronous execution ensures:\nProjections are updated before the command completes Exceptions propagate immediately Read-your-writes consistency within the same transaction Event handlers execute after events are persisted to the EventStore but before the command handler returns. This guarantees that projections reflect the latest state when the command completes successfully.\nHandling errors in event handlers Event handlers can choose how to handle errors based on the operation\u0026rsquo;s criticality.\nFor critical operations like projection updates, let exceptions propagate to halt processing:\nprivate function handleCourseDefinedEvent( CourseDefinedEvent $event, RecordedEvent $recordedEvent, ): void { $list = $this-\u0026gt;getList(); $list-\u0026gt;defineCourse($event-\u0026gt;courseId, $event-\u0026gt;name, $event-\u0026gt;capacity); // If storing fails, exception propagates and command fails $this-\u0026gt;projections-\u0026gt;store($list); }\rFor non-critical operations like sending notifications, catch exceptions to prevent blocking:\nprivate function handleStudentSubscribedToCourseEvent( StudentSubscribedToCourseEvent $event, RecordedEvent $recordedEvent, ): void { try { $this-\u0026gt;mailer-\u0026gt;send( to: $event-\u0026gt;studentId, subject: \u0026#39;Subscription Confirmed\u0026#39;, body: $this-\u0026gt;buildMessage($event) ); } catch (MailerException $e) { // Log but don\u0026#39;t halt processing $this-\u0026gt;logger-\u0026gt;warning(\u0026#39;Failed to send notification\u0026#39;, [ \u0026#39;event\u0026#39; =\u0026gt; $event::class, \u0026#39;error\u0026#39; =\u0026gt; $e-\u0026gt;getMessage(), ]); } }\rBest practices Keep handlers focused. Each handler should have a single responsibility. Don\u0026rsquo;t mix projection updates with external API calls in the same handler.\nHandle missing data gracefully. If a resource doesn\u0026rsquo;t exist when handling an event, create it or handle the absence appropriately rather than letting the handler fail.\nMake side effects idempotent. External operations like sending emails should be idempotent or use deduplication mechanisms to prevent duplicate actions during replays.\nLet critical failures propagate. For essential operations like projection updates, let exceptions propagate to halt processing.\nCatch non-critical failures. For optional operations like notifications, catch exceptions and log them without blocking the event flow.\nUse metadata when needed. Access the RecordedEvent parameter to retrieve correlation IDs, timestamps, or other metadata for logging and tracing.\nEvent handlers are essential for the read side of your CQRS architecture. The most common use of event handlers is updating projections, which we\u0026rsquo;ll explore in the next section.\n","date":"0001-01-01","id":15,"permalink":"/docs/cqrs/reacting-with-event-handlers/","summary":"\u003cp\u003eEvent handlers respond to events by updating projections, triggering side effects, or integrating with external systems.\nThey subscribe to specific event types and execute synchronously when those events are published.\u003c/p\u003e","tags":[],"title":"Reacting with Event Handlers"},{"content":"Event sourcing is a pattern where you store the state of your application as a sequence of events rather than just the current state. Instead of updating records in place, you append new events describing what happened.\nEvents as source of truth In traditional CRUD applications, you store the current state:\nUPDATE students SET name = \u0026#39;Alice Johnson\u0026#39; WHERE id = 123;\rWith event sourcing, you store what happened:\nStudentRegistered(id: 123, name: \u0026#39;Alice Johnson\u0026#39;) StudentNameChanged(id: 123, from: \u0026#39;Alice Johnson\u0026#39;, to: \u0026#39;Alice Cooper\u0026#39;)\rThe current state is derived by replaying these events in order.\nReplaying events to reconstruct state Systems rebuild state by processing events sequentially:\nstate = EmptyState for each event in events: state = apply(state, event)\rThis replay mechanism enables:\nAudit trail: Complete history of all changes Debugging: Replay events to understand how the system reached its current state Time travel: Query past states by replaying events up to a specific point Benefits of event sourcing Complete audit trail. Every change is recorded as an immutable event, providing a full history for compliance, debugging, and analysis.\nDebugging and troubleshooting. Replay events to reproduce bugs or understand how the system reached a problematic state.\nFlexible projections. Build multiple read models from the same events, optimized for different queries.\nBusiness insights. Analyze event history to understand user behavior, identify trends, and make data-driven decisions.\n","date":"0001-01-01","id":16,"permalink":"/docs/event-sourcing/understanding-event-sourcing/","summary":"\u003cp\u003eEvent sourcing is a pattern where you store the state of your application as a sequence of events rather than just the\ncurrent state. Instead of updating records in place, you append new events describing what happened.\u003c/p\u003e","tags":[],"title":"Understanding Event Sourcing"},{"content":"The bootstrap file provides a single entry point for configuring your container and booting your application. This file can be included in CLI scripts, web applications, and tests to ensure consistent initialization.\nCreating the bootstrap file Create a bootstrap.php file at your project root:\n\u0026lt;?php declare(strict_types=1); namespace MyApp; use Backslash\\CommandDispatcher\\DispatcherInterface; use Backslash\\CommandDispatcher\\HandlerProxy; use Backslash\\EventBus\\EventBusInterface; use Backslash\\EventBus\\EventHandlerProxy; use Psr\\Container\\ContainerInterface; return (function (): ContainerInterface { const COMMAND_HANDLERS = [ CourseCommandHandler::class =\u0026gt; [ ChangeCourseCapacityCommand::class, DefineCourseCommand::class, ], StudentCommandHandler::class =\u0026gt; [ RegisterStudentCommand::class, ], SubscriptionCommandHandler::class =\u0026gt; [ SubscribeStudentToCourseCommand::class, UnsubscribeStudentFromCourseCommand::class, ], ]; const PROJECTORS = [ CourseListProjector::class =\u0026gt; [ CourseDefinedEvent::class, CourseCapacityChangedEvent::class, StudentSubscribedToCourseEvent::class, StudentUnsubscribedFromCourseEvent::class, ], StudentListProjector::class =\u0026gt; [ CourseDefinedEvent::class, StudentRegisteredEvent::class, StudentSubscribedToCourseEvent::class, StudentUnsubscribedFromCourseEvent::class, ], ]; const PROCESSORS = [ NotificationProcessor::class =\u0026gt; [ StudentSubscribedToCourseEvent::class, StudentUnsubscribedFromCourseEvent::class, ], EmailProcessor::class =\u0026gt; [ StudentRegisteredEvent::class, ], ]; chdir(__DIR__); require \u0026#39;vendor/autoload.php\u0026#39;; // Load application configuration $config = include \u0026#39;config.php\u0026#39;; // Get your framework\u0026#39;s container or instantiate your own implementation /** @var ContainerInterface $container */ $container = new MyAppContainer($config); // Boot process: register command handlers foreach (COMMAND_HANDLERS as $handlerClass =\u0026gt; $commandClasses) { foreach ($commandClasses as $commandClass) { $container-\u0026gt;get(DispatcherInterface::class)-\u0026gt;registerHandler( $commandClass, new HandlerProxy(fn () =\u0026gt; $container-\u0026gt;get($handlerClass)) ); } } // Boot process: register event handlers foreach (PROJECTORS as $projectorClass =\u0026gt; $eventClasses) { foreach ($eventClasses as $eventClass) { $container-\u0026gt;get(EventBusInterface::class)-\u0026gt;subscribe( $eventClass, new EventHandlerProxy(fn () =\u0026gt; $container-\u0026gt;get($projectorClass)) ); } } // Register processors (skip during projection rebuilds) $isRebuildMode = (bool) getenv(\u0026#39;REBUILD_PROJECTIONS\u0026#39;); if (!$isRebuildMode) { foreach (PROCESSORS as $processorClass =\u0026gt; $eventClasses) { foreach ($eventClasses as $eventClass) { $container-\u0026gt;get(EventBusInterface::class)-\u0026gt;subscribe( $eventClass, new EventHandlerProxy(fn () =\u0026gt; $container-\u0026gt;get($processorClass)) ); } } } return $container; })();\rThis file loads the autoloader, retrieves the configured container, executes the boot process, and returns the ready-to-use container.\nUsing in CLI scripts Include the bootstrap in command-line scripts:\n\u0026lt;?php declare(strict_types=1); use Psr\\Container\\ContainerInterface; use Backslash\\CommandDispatcher\\DispatcherInterface; use Demo\\Application\\Command\\Course\\DefineCourseCommand; /** @var ContainerInterface $container */ $container = require __DIR__ . \u0026#39;/../bootstrap.php\u0026#39;; /** @var DispatcherInterface $dispatcher */ $dispatcher = $container-\u0026gt;get(DispatcherInterface::class); // Execute commands $dispatcher-\u0026gt;dispatch(new DefineCourseCommand(\u0026#39;101\u0026#39;, \u0026#39;Introduction to PHP\u0026#39;, 30)); $dispatcher-\u0026gt;dispatch(new DefineCourseCommand(\u0026#39;102\u0026#39;, \u0026#39;Advanced PHP\u0026#39;, 20)); echo \u0026#34;Courses created successfully\\n\u0026#34;;\rThis pattern makes all components available for administrative tasks, data imports, or cron jobs.\nUsing in web applications Include the bootstrap in your web entry point (e.g., public/index.php).\nHere\u0026rsquo;s an example using Slim Framework:\n\u0026lt;?php declare(strict_types=1); use Psr\\Container\\ContainerInterface; use Slim\\Factory\\AppFactory; /** @var ContainerInterface $container */ $container = require __DIR__ . \u0026#39;/../bootstrap.php\u0026#39;; // Set container to create App with on AppFactory AppFactory::setContainer($container); $app = AppFactory::create(); // Define routes $app-\u0026gt;get(\u0026#39;/students/{id}\u0026#39;, function ($request, $response) use ($container) { $projectionStore = $container-\u0026gt;get(ProjectionStoreInterface::class); $student = $projectionStore-\u0026gt;find( $request-\u0026gt;getAttribute(\u0026#39;id\u0026#39;), StudentProjection::class ); $response-\u0026gt;getBody()-\u0026gt;write(json_encode($student)); return $response-\u0026gt;withHeader(\u0026#39;Content-Type\u0026#39;, \u0026#39;application/json\u0026#39;); }); $app-\u0026gt;post(\u0026#39;/courses\u0026#39;, function ($request, $response) use ($container) { $data = $request-\u0026gt;getParsedBody(); $dispatcher = $container-\u0026gt;get(DispatcherInterface::class); $dispatcher-\u0026gt;dispatch( new DefineCourseCommand($data[\u0026#39;id\u0026#39;], $data[\u0026#39;name\u0026#39;], $data[\u0026#39;capacity\u0026#39;]) ); return $response-\u0026gt;withStatus(201); }); // Run application $app-\u0026gt;run();\rThe bootstrap provides all services needed to handle HTTP requests.\nUsing in tests Include the bootstrap in your test base class:\n\u0026lt;?php namespace Tests; use Backslash\\CommandDispatcher\\DispatcherInterface; use Backslash\\EventBus\\EventBusInterface; use Backslash\\EventStore\\EventStoreInterface; use Backslash\\ProjectionStore\\ProjectionStoreInterface; use Backslash\\Scenario\\Scenario; use Backslash\\Scenario\\AssertionsTrait; use PHPUnit\\Framework\\TestCase as PHPUnitTestCase; use Psr\\Container\\ContainerInterface; abstract class TestCase extends PHPUnitTestCase { use AssertionsTrait; protected Scenario $scenario; private ContainerInterface $container; public function setUp(): void { parent::setUp(); /** @var ContainerInterface $container */ $this-\u0026gt;container = require __DIR__ . \u0026#39;/../bootstrap.php\u0026#39;; $this-\u0026gt;scenario = new Scenario( $this-\u0026gt;container-\u0026gt;get(EventBusInterface::class), $this-\u0026gt;container-\u0026gt;get(DispatcherInterface::class), $this-\u0026gt;container-\u0026gt;get(ProjectionStoreInterface::class), $this-\u0026gt;container-\u0026gt;get(EventStoreInterface::class), ); } }\rTests use the same bootstrap as production, ensuring consistency.\nBest practices Use your framework\u0026rsquo;s container. If your application uses a framework like Laravel or Symfony, use its built-in container rather than implementing your own.\nUse established container libraries. For framework-agnostic applications, use mature containers like php-di/php-di or league/container that provide auto-wiring and performance optimizations.\nKeep bootstrap centralized. All component configuration and boot logic should be accessible from the bootstrap file.\nUse environment variables. Keep environment-specific settings like database credentials and API keys out of code.\nUse the same bootstrap everywhere. CLI scripts, web entry points, and tests should all use the same bootstrap file to ensure consistency.\nExecute boot once. The boot process (handler registration) should happen once during application initialization, not on every request.\n","date":"0001-01-01","id":17,"permalink":"/docs/application-setup/using-the-bootstrap-file/","summary":"\u003cp\u003eThe bootstrap file provides a single entry point for configuring your container and booting your application. This file\ncan be included in CLI scripts, web applications, and tests to ensure consistent initialization.\u003c/p\u003e","tags":[],"title":"Using the Bootstrap File"},{"content":"Projections are read-optimized views of your system\u0026rsquo;s state. Unlike Models, which are rebuilt from events to make decisions, projections are designed specifically for querying. They represent the read side of your CQRS architecture.\nProjections are data structures (read models) that represent the current state; projectors are event handlers that update these projections when events occur. This separation keeps projection logic organized and testable.\nProjections work with the ProjectionStore, which handles their persistence and retrieval. We\u0026rsquo;ll cover the ProjectionStore in detail later in this section.\nUnderstanding projections A projection is a denormalized view built from events. It answers specific questions about your system\u0026rsquo;s state without requiring complex joins or event replay. For example:\nA list of all courses with their current capacity and available seats A student\u0026rsquo;s enrollment history with course details A dashboard showing real-time subscription metrics Projections are updated synchronously after events are published. Because event handlers execute in the same PHP process as command dispatch, projections are immediately consistent with the write side once the command completes.\nProjections are often designed for specific UI needs. They can expose their data in convenient formats like arrays for JSON serialization or XML objects for API responses. This flexibility allows you to tailor each projection to its intended consumer.\nProjections vs Models The distinction between projections and models is fundamental to CQRS:\nModels (write side):\nRebuilt from events for each decision Enforce business rules Not persisted; exist only during command processing Optimized for making decisions Projections (read side):\nContinuously updated as events occur Persisted for efficient querying No business logic; simple data structures Optimized for reading and display Defining projections Projections must implement ProjectionInterface:\ninterface ProjectionInterface { public function getId(): string; }\rThe getId() method returns the projection\u0026rsquo;s identifier. A projection\u0026rsquo;s identity is composed of both its ID and its fully qualified class name, which means two different projection classes can use the same ID without conflict.\nHere\u0026rsquo;s a simple projection example:\nclass CourseProjection implements ProjectionInterface { private string $courseId; private string $name; private int $capacity; public function __construct(string $courseId, string $name, int $capacity) { $this-\u0026gt;courseId = $courseId; $this-\u0026gt;name = $name; $this-\u0026gt;capacity = $capacity; } public function getId(): string { return $this-\u0026gt;courseId; } public function getName(): string { return $this-\u0026gt;name; } public function getCapacity(): int { return $this-\u0026gt;capacity; } public function changeCapacity(int $newCapacity): void { $this-\u0026gt;capacity = $newCapacity; } }\rThis projection represents a course with methods to access its data and update its capacity. Each course gets its own projection instance identified by its course ID.\nExposing projection data Projections often need to expose their data in formats suitable for APIs or user interfaces. Implement JsonSerializable to control JSON representation:\nclass CourseProjection implements ProjectionInterface, JsonSerializable { private string $courseId; private string $name; private int $capacity; public function __construct(string $courseId, string $name, int $capacity) { $this-\u0026gt;courseId = $courseId; $this-\u0026gt;name = $name; $this-\u0026gt;capacity = $capacity; } public function getId(): string { return $this-\u0026gt;courseId; } public function getName(): string { return $this-\u0026gt;name; } public function getCapacity(): int { return $this-\u0026gt;capacity; } public function changeCapacity(int $newCapacity): void { $this-\u0026gt;capacity = $newCapacity; } public function jsonSerialize(): array { return [ \u0026#39;id\u0026#39; =\u0026gt; $this-\u0026gt;courseId, \u0026#39;name\u0026#39; =\u0026gt; $this-\u0026gt;name, \u0026#39;capacity\u0026#39; =\u0026gt; $this-\u0026gt;capacity, ]; } }\rThis allows seamless JSON encoding:\n$projection = $projectionStore-\u0026gt;find(\u0026#39;course-123\u0026#39;, CourseProjection::class); $json = json_encode($projection); // {\u0026#34;id\u0026#34;:\u0026#34;course-123\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;PHP Basics\u0026#34;,\u0026#34;capacity\u0026#34;:30} Similarly, projections can provide methods for XML, CSV, or any other format your application needs:\npublic function toXml(): SimpleXMLElement { $xml = new SimpleXMLElement(\u0026#39;\u0026lt;course/\u0026gt;\u0026#39;); $xml-\u0026gt;addChild(\u0026#39;id\u0026#39;, $this-\u0026gt;courseId); $xml-\u0026gt;addChild(\u0026#39;name\u0026#39;, $this-\u0026gt;name); $xml-\u0026gt;addChild(\u0026#39;capacity\u0026#39;, (string) $this-\u0026gt;capacity); return $xml; } public function toArray(): array { return [ \u0026#39;id\u0026#39; =\u0026gt; $this-\u0026gt;courseId, \u0026#39;name\u0026#39; =\u0026gt; $this-\u0026gt;name, \u0026#39;capacity\u0026#39; =\u0026gt; $this-\u0026gt;capacity, ]; }\rDesigning projection structure Design your projections based on how you need to query them, not how your domain is modeled. A single domain concept might have multiple projections for different use cases.\nMultiple specialized projections may seem redundant, but they enable optimal query performance; each projection can be indexed and structured for its specific access pattern.\n// For listing all courses class CourseListItemProjection implements ProjectionInterface { public function __construct( public string $id, public string $name, public int $availableSeats, ) { } public function getId(): string { return $this-\u0026gt;id; } } // For displaying course details class CourseDetailProjection implements ProjectionInterface { public function __construct( public string $id, public string $name, public string $description, public int $capacity, public int $subscribedCount, public array $subscribedStudents, ) { } public function getId(): string { return $this-\u0026gt;id; } } // For analytics class CourseMetricsProjection implements ProjectionInterface { public function __construct( public string $id, public int $totalSubscriptions, public int $totalUnsubscriptions, public float $averageSubscriptionDuration, ) { } public function getId(): string { return $this-\u0026gt;id; } }\rEach projection is tailored to answer specific queries efficiently.\nWorking with ProjectionStore The ProjectionStore is responsible for persisting and retrieving projections. It uses an adapter to interact with the underlying storage mechanism.\nBackslash provides two built-in adapters:\nPdoProjectionStoreAdapter: For PDO-compatible databases (MySQL, SQLite); suitable for production use InMemoryProjectionStoreAdapter: In-memory storage; useful for testing with the Scenario component Developers can build custom adapters for other storage engines like MongoDB, Redis, or even a file system by implementing the AdapterInterface.\nSetting up the ProjectionStore Create a ProjectionStore instance with a storage adapter during your application\u0026rsquo;s bootstrap:\n$pdo = new PdoProxy(fn() =\u0026gt; new PDO(\u0026#39;mysql:host=localhost;dbname=myapp\u0026#39;, \u0026#39;user\u0026#39;, \u0026#39;pass\u0026#39;)); $serializer = new Serializer(new SerializeFunctionSerializer()); $config = new Config(); $adapter = new PdoProjectionStoreAdapter($pdo, $serializer, $config); $projectionStore = new ProjectionStore($adapter);\rThe PdoProjectionStoreAdapter requires:\nPDO connection: Database connection via PdoProxy or direct PDO instance Serializer: Converts projections to/from storage format; SerializeFunctionSerializer uses PHP\u0026rsquo;s native serialize() function Config (optional): Allows customization of database column names via withTable() and withAlias() methods Example with custom configuration:\n$config = (new Config()) -\u0026gt;withTable(\u0026#39;my_projections\u0026#39;) -\u0026gt;withAlias(\u0026#39;projection_id\u0026#39;, \u0026#39;id\u0026#39;) -\u0026gt;withAlias(\u0026#39;projection_class\u0026#39;, \u0026#39;type\u0026#39;) -\u0026gt;withAlias(\u0026#39;projection_payload\u0026#39;, \u0026#39;data\u0026#39;); $adapter = new PdoProjectionStoreAdapter($pdo, $serializer, $config);\rFor testing, use the InMemoryProjectionStoreAdapter:\n$adapter = new InMemoryProjectionStoreAdapter(); $projectionStore = new ProjectionStore($adapter);\rThis adapter stores projections in memory, making it fast and isolated for test scenarios.\nCommitting automatically after command handling Backslash provides ProjectionStoreTransactionCommandDispatcherMiddleware, a command dispatcher middleware that manages projection transactions automatically. It wraps the command dispatch process in a transaction and calls commit() on the ProjectionStore when the command completes successfully. If an exception occurs during command processing, the middleware calls rollback() to discard buffered changes.\nuse Backslash\\ProjectionStoreTransactionCommandDispatcherMiddleware\\ProjectionStoreTransactionCommandDispatcherMiddleware; $dispatcher-\u0026gt;addMiddleware( new ProjectionStoreTransactionCommandDispatcherMiddleware($projectionStore) );\rThis middleware is typically added to the command dispatcher during application bootstrap, ensuring all commands benefit from automatic transaction management without requiring explicit commit/rollback calls in command handlers.\nCreating projectors Projectors are event handlers that update projections. Here\u0026rsquo;s a complete example:\nclass CourseProjector implements EventHandlerInterface { use EventHandlerTrait; public function __construct( private ProjectionStoreInterface $projections, ) { } private function handleCourseDefinedEvent( CourseDefinedEvent $event, RecordedEvent $recordedEvent, ): void { $projection = new CourseProjection( $event-\u0026gt;courseId, $event-\u0026gt;name, $event-\u0026gt;capacity ); $this-\u0026gt;projections-\u0026gt;store($projection); } private function handleCourseCapacityChangedEvent( CourseCapacityChangedEvent $event, RecordedEvent $recordedEvent, ): void { try { $projection = $this-\u0026gt;projections-\u0026gt;find( $event-\u0026gt;courseId, CourseProjection::class ); $projection-\u0026gt;changeCapacity($event-\u0026gt;new); $this-\u0026gt;projections-\u0026gt;store($projection); } catch (ProjectionNotFoundException) { // Log or handle missing projection } } }\rRegister the projector with the EventBus:\n$projector = new CourseProjector($projectionStore); $eventBus-\u0026gt;subscribe(CourseDefinedEvent::class, $projector); $eventBus-\u0026gt;subscribe(CourseCapacityChangedEvent::class, $projector);\rQuerying projections Once projections are persisted, you can query them in your application or UI layer:\n// In a request handler or controller public function handle(ServerRequestInterface $request): ResponseInterface { $courseId = $request-\u0026gt;getAttribute(\u0026#39;courseId\u0026#39;); $projection = $this-\u0026gt;projectionStore-\u0026gt;find($courseId, CourseProjection::class) return new JsonResponse($projection); }\rThis separation between write operations (commands updating models) and read operations (queries fetching projections) is the essence of CQRS.\nBest practices Design for queries. Structure projections based on how you need to query them, not how your domain is modeled.\nCreate multiple projections. Don\u0026rsquo;t try to create one projection that serves all queries. Build specialized projections for different use cases.\nUse buffering for consistency. The buffering mechanism enables batch updates and transactional consistency. Use middleware to manage commit/rollback automatically.\nKeep projections simple. Projections should be dumb data structures. Complex logic belongs in the projectors that update them.\nKeep projectors autonomous. Projectors should only read and modify the projections they own; avoid reading other projections. If you need data from multiple projections, create a new combined projection instead.\nOptimize for reads. Precompute values, denormalize data, and structure projections to make queries fast.\nOne projection instance per entity. Each entity (course, student, etc.) should have its own projection instance with a unique ID.\nMake projections serializable. Projections must be serializable for persistence. Use simple types and avoid circular references.\n","date":"0001-01-01","id":18,"permalink":"/docs/cqrs/building-projections/","summary":"\u003cp\u003eProjections are read-optimized views of your system\u0026rsquo;s state. Unlike Models, which are rebuilt from events to make\ndecisions, projections are designed specifically for querying. They represent the read side of your CQRS architecture.\u003c/p\u003e","tags":[],"title":"Building Projections"},{"content":"Events represent immutable facts about what happened in your system. They are the building blocks of event sourcing and capture domain changes as a permanent record.\nCreating event classes In Backslash, events are simple classes that implement the EventInterface:\nreadonly class StudentRegisteredEvent implements EventInterface { use ToArrayTrait; // Handles automatic serialization public function __construct( public string $studentId, public string $name, ) { } public function getIdentifiers(): Identifiers { return new Identifiers([ \u0026#39;studentId\u0026#39; =\u0026gt; $this-\u0026gt;studentId, ]); } }\rThis is a simple single-entity model. For more complex scenarios, see CourseSubscriptionModel in the demo application, which demonstrates a multi-entity model that loads events from both students and courses to enforce rules that span multiple entities.\nThe readonly keyword is recommended to enforce immutability; once created, the event cannot be modified.\nImplementing the EventInterface All events must implement Backslash\\Event\\EventInterface:\ninterface EventInterface { public function getIdentifiers(): Identifiers; public function toArray(): array; public static function fromArray(array $data): self; }\rThe getIdentifiers() method returns identifiers used to scope the event. The toArray() and fromArray() methods handle serialization for storage and retrieval.\nWorking with identifiers Backslash events can include identifiers for multiple entities, following the Dynamic Consistency Boundary specification. Identifiers determine which events belong together when enforcing business rules.\nThe StudentSubscribedToCourseEvent includes identifiers for both student and course because the subscription action involves both entities:\npublic function getIdentifiers(): Identifiers { return new Identifiers([ \u0026#39;studentId\u0026#39; =\u0026gt; $this-\u0026gt;studentId, \u0026#39;courseId\u0026#39; =\u0026gt; $this-\u0026gt;courseId, ]); }\rThis multi-entity identification enables dynamic consistency boundaries. When enforcing the rule \u0026ldquo;a student can only subscribe if the course isn\u0026rsquo;t full\u0026rdquo;, you need events from both the student and the course within the same consistency boundary.\nCompare this with CourseDefinedEvent, which only identifies the course:\nreadonly class CourseDefinedEvent implements EventInterface { use ToArrayTrait; public function __construct( public string $courseId, public string $name, public int $capacity, ) { } public function getIdentifiers(): Identifiers { return new Identifiers([ \u0026#39;courseId\u0026#39; =\u0026gt; $this-\u0026gt;courseId, ]); } }\rDifferent events need different identifiers depending on which entities participated in the action and which consistency boundaries they support.\nSerializing events Backslash provides ToArrayTrait that uses reflection to automatically implement toArray() and fromArray(). All properties must be scalar types (string, int, float, bool, or null) or arrays of scalar types. Constructor parameters must match the properties exactly by name and type.\n$event = new StudentSubscribedToCourseEvent(\u0026#39;123\u0026#39;, \u0026#39;456\u0026#39;); $serialized = $event-\u0026gt;toArray(); // [\u0026#39;studentId\u0026#39; =\u0026gt; \u0026#39;123\u0026#39;, \u0026#39;courseId\u0026#39; =\u0026gt; \u0026#39;456\u0026#39;] $restored = StudentSubscribedToCourseEvent::fromArray($serialized);\rToArrayTrait also supports variadic constructor parameters, which is useful to document the expected type of array elements directly in the constructor signature (e.g. string ...$userIds instead of array $userIds).\nFor events with value objects or complex structures, implement toArray() and fromArray() manually:\nreadonly class CourseScheduleChangedEvent implements EventInterface { public function __construct( public string $courseId, public DateTimeImmutable $startDate, public DateTimeImmutable $endDate, ) { } public function getIdentifiers(): Identifiers { return new Identifiers([ \u0026#39;courseId\u0026#39; =\u0026gt; $this-\u0026gt;courseId, ]); } public function toArray(): array { return [ \u0026#39;courseId\u0026#39; =\u0026gt; $this-\u0026gt;courseId, \u0026#39;startDate\u0026#39; =\u0026gt; $this-\u0026gt;startDate-\u0026gt;format(\u0026#39;Y-m-d\u0026#39;), \u0026#39;endDate\u0026#39; =\u0026gt; $this-\u0026gt;endDate-\u0026gt;format(\u0026#39;Y-m-d\u0026#39;), ]; } public static function fromArray(array $data): self { return new self( $data[\u0026#39;courseId\u0026#39;], new DateTimeImmutable($data[\u0026#39;startDate\u0026#39;]), new DateTimeImmutable($data[\u0026#39;endDate\u0026#39;]), ); } }\rBest practices Use past tense for event names. Events describe completed actions: StudentRegisteredEvent, not RegisterStudentEvent; CourseCapacityChangedEvent, not ChangeCourseCapacityEvent.\nKeep events immutable. Mark classes as readonly and use public readonly properties, or use private properties with public getters. Never modify an event after creation.\nInclude all relevant identifiers. Add identifiers for every entity that participated in the event, not just a single aggregate root.\nStore facts, not derived data. Events should contain essential information about what happened, not calculations or data derivable from other events.\nKeep events focused. Each event should represent a single fact. Avoid capturing multiple unrelated changes in one event.\nConsider recording both old and new values. For state changes, recording previous and new values provides a complete audit trail.\nMake serialization explicit for complex types. If your event contains value objects or non-scalar types, implement toArray() and fromArray() manually rather than relying on automatic serialization.\n","date":"0001-01-01","id":19,"permalink":"/docs/event-sourcing/defining-events/","summary":"\u003cp\u003eEvents represent immutable facts about what happened in your system. They are the building blocks of event sourcing and\ncapture domain changes as a permanent record.\u003c/p\u003e","tags":[],"title":"Defining Events"},{"content":"","date":"0001-01-01","id":20,"permalink":"/docs/testing/","summary":"","tags":[],"title":"Testing Your Application"},{"content":"","date":"0001-01-01","id":21,"permalink":"/docs/customization/","summary":"","tags":[],"title":"Customization"},{"content":"Queries define which events should be loaded from the EventStore. They act as dynamic consistency boundaries, allowing you to load exactly the events needed to make a specific decision. The boundaries are determined at runtime based on the operation being performed, not predefined at design time.\nQueries also play a crucial role in optimistic concurrency control. Before appending new events, the query is re-executed to verify that no other process has added events to the stream in the meantime.\nUnderstanding queries Queries implement QueryInterface and describe which events to fetch based on event class and identifiers.\nHere are some basic query examples:\n// Load all events of a specific type EventClass::is(StudentRegisteredEvent::class) // Load events for a specific entity Identifier::is(\u0026#39;studentId\u0026#39;, \u0026#39;student-123\u0026#39;) // Combine filters EventClass::is(StudentRegisteredEvent::class) -\u0026gt;and(Identifier::is(\u0026#39;studentId\u0026#39;, \u0026#39;student-123\u0026#39;))\rThe demo includes a static method on each Model to build its query. This is a convenient convention but not required by Backslash; queries can be created anywhere in your application.\nHere\u0026rsquo;s a simple case for CourseCapacityModel:\npublic static function buildQuery(string $courseId): QueryInterface { return EventClass::in( CourseCapacityChangedEvent::class, CourseDefinedEvent::class, )-\u0026gt;and(Identifier::is(\u0026#39;courseId\u0026#39;, $courseId)); }\rThis query loads events related to a single course\u0026rsquo;s capacity.\nBuilding queries with EventClass and Identifier Queries combine event class filters with identifier filters:\nEventClass filters:\nEventClass::is(EventClass::class) - Single event type EventClass::in(Event1::class, Event2::class) - Multiple event types Identifier filters:\nIdentifier::is('key', 'value') - Exact identifier match Multiple identifiers can be combined Combining filters:\n-\u0026gt;and() - Both conditions must match -\u0026gt;or() - Either condition must match Building multi-entity queries The CourseSubscriptionModel demonstrates a more complex query spanning multiple entities:\npublic static function buildQuery(string $studentId, string $courseId): QueryInterface { $eventForThisCourseLifecycle = EventClass::in( CourseCapacityChangedEvent::class, CourseDefinedEvent::class, )-\u0026gt;and(Identifier::is(\u0026#39;courseId\u0026#39;, $courseId)); $eventsForThisStudentLifecycle = EventClass::is( StudentRegisteredEvent::class, )-\u0026gt;and(Identifier::is(\u0026#39;studentId\u0026#39;, $studentId)); $eventsForThisStudentSubscriptions = EventClass::in( StudentUnsubscribedFromCourseEvent::class, StudentSubscribedToCourseEvent::class, )-\u0026gt;and(Identifier::is(\u0026#39;studentId\u0026#39;, $studentId)); $eventsForSubscriptionsToThisCourse = EventClass::in( StudentSubscribedToCourseEvent::class, StudentUnsubscribedFromCourseEvent::class, )-\u0026gt;and(Identifier::is(\u0026#39;courseId\u0026#39;, $courseId)); return $eventForThisCourseLifecycle -\u0026gt;or($eventsForThisStudentLifecycle) -\u0026gt;or($eventsForThisStudentSubscriptions) -\u0026gt;or($eventsForSubscriptionsToThisCourse); }\rThis query loads:\nThe course\u0026rsquo;s lifecycle events (definition and capacity changes) The student\u0026rsquo;s registration All of this student\u0026rsquo;s subscription events All subscription events for this course (to count enrollments) This multi-entity boundary enables the Model to enforce rules like \u0026ldquo;a student can only subscribe if the course isn\u0026rsquo;t full\u0026rdquo; and \u0026ldquo;a student can subscribe to at most 3 courses\u0026rdquo;.\nBest practices Build queries per decision. Each business decision should define its own query that loads exactly the events needed.\nStart narrow, expand as needed. Begin with the minimal set of events and expand the query only when business rules require additional context.\nDocument complex queries. Multi-entity queries can be intricate; add comments explaining what events are loaded and why.\nUse static factory methods. Place query building logic in static methods on your models for discoverability.\nKeep queries focused. Don\u0026rsquo;t load events that aren\u0026rsquo;t needed for the decision being made.\n","date":"0001-01-01","id":22,"permalink":"/docs/event-sourcing/querying-events/","summary":"\u003cp\u003eQueries define which events should be loaded from the EventStore. They act as dynamic consistency boundaries, allowing\nyou to load exactly the events needed to make a specific decision. The boundaries are determined at runtime based on the\noperation being performed, not predefined at design time.\u003c/p\u003e","tags":[],"title":"Querying Events"},{"content":"","date":"0001-01-01","id":23,"permalink":"/docs/application-setup/","summary":"","tags":[],"title":"Application Setup"},{"content":"Models are decision-making components that encapsulate domain logic. They are rebuilt from events and record new events when business rules are satisfied.\nIntroducing the Repository Before diving into models, it\u0026rsquo;s important to understand the Repository. The Repository is a core Backslash component responsible for:\nLoading events based on a query Replaying those events into a model Persisting new events recorded by the model Publishing events to the EventBus You\u0026rsquo;ll use the Repository in command handlers like this:\n// Load a model using a query $model = $repository-\u0026gt;loadModel(ModelClass::class, $query); // Execute business logic $model-\u0026gt;makeDecision(); // Persist changes $repository-\u0026gt;storeChanges($model);\rCreating model classes Models extend AbstractModel, which implements ModelInterface:\ninterface ModelInterface { public function getChanges(): RecordedEventStream; public function clearChanges(): void; public function applyEvents(RecordedEventStream $stream): void; }\rThese methods allow the Repository to:\nLoad events into the model with applyEvents() Retrieve new events recorded by the model with getChanges() Clear recorded events after persistence with clearChanges() Here\u0026rsquo;s an example of a model extending AbstractModel:\nuse Backslash\\Model\\AbstractModel; class CourseCapacityModel extends AbstractModel { private ?string $courseId = null; private int $capacity = 0; private bool $defined = false; public function change(int $newCapacity): void { if (!$this-\u0026gt;defined) { throw new CourseNotDefinedException(); } if ($newCapacity === $this-\u0026gt;capacity) { return; // Idempotent } $this-\u0026gt;record(new CourseCapacityChangedEvent( courseId: $this-\u0026gt;courseId, old: $this-\u0026gt;capacity, new: $newCapacity )); } protected function applyCourseDefinedEvent(CourseDefinedEvent $event): void { $this-\u0026gt;courseId = $event-\u0026gt;courseId; $this-\u0026gt;capacity = $event-\u0026gt;capacity; $this-\u0026gt;defined = true; } protected function applyCourseCapacityChangedEvent( CourseCapacityChangedEvent $event ): void { $this-\u0026gt;capacity = $event-\u0026gt;new; } }\rReplaying events with apply methods Apply methods rebuild the model\u0026rsquo;s state from events. They follow the naming pattern apply{EventClassName}:\nprotected function applyCourseCapacityChangedEvent( CourseCapacityChangedEvent $event ): void { $this-\u0026gt;capacity = $event-\u0026gt;new; }\rApply methods are invoked automatically during event replay. They update internal state based on event data without performing validation or business logic. Think of them as pure state transitions.\nCreating decision methods Public methods enforce business rules. They inspect internal state, validate input, check for idempotency, and record new events using $this-\u0026gt;record() when rules are satisfied:\npublic function change(int $newCapacity): void { // Validate preconditions if (!$this-\u0026gt;defined) { throw new CourseNotDefinedException(); } // Check idempotency if ($newCapacity === $this-\u0026gt;capacity) { return; } // Record the decision $this-\u0026gt;record(new CourseCapacityChangedEvent( courseId: $this-\u0026gt;courseId, old: $this-\u0026gt;capacity, new: $newCapacity )); }\rDecision methods express business logic clearly. They throw domain exceptions when rules are violated and record events when decisions are valid.\nRecording new events Use $this-\u0026gt;record() to append new events:\n$this-\u0026gt;record(new StudentRegisteredEvent( studentId: $studentId, name: $name ));\rThe record() method does two things:\nAutomatically calls the corresponding apply method, updating the model\u0026rsquo;s state immediately Adds the event to the model\u0026rsquo;s internal list of changes This means recorded events are:\nApplied immediately to the model via apply methods Returned by getChanges() for persistence Not yet persisted until Repository::storeChanges() is called Note that you use $this-\u0026gt;record() rather than returning events because AbstractModel manages the internal event stream. This design allows models to record multiple events for complex operations while maintaining clean method signatures.\nBest practices Keep models focused. Each model should enforce a specific set of business rules. Don\u0026rsquo;t create god models that try to enforce everything.\nUse domain exceptions. When business rules are violated, throw descriptive exceptions that explain what went wrong.\nCheck idempotency. Before recording events, verify the operation isn\u0026rsquo;t redundant.\nApply methods are pure state transitions. Never perform validation or business logic in apply methods; they only update state.\nDecision methods contain business logic. All validation, rule enforcement, and conditional logic belongs in public decision methods.\nRecord events, don\u0026rsquo;t return them. Use $this-\u0026gt;record() to append events; don\u0026rsquo;t return events from decision methods.\nModels are stateful during their lifetime. A model instance retains its state throughout the request. Load it once, make decisions, then store changes.\nOne model instance per decision. Don\u0026rsquo;t reuse a model instance for multiple unrelated decisions. Load a fresh model for each command.\n","date":"0001-01-01","id":24,"permalink":"/docs/event-sourcing/building-models/","summary":"\u003cp\u003eModels are decision-making components that encapsulate domain logic. They are rebuilt from events and record new events\nwhen business rules are satisfied.\u003c/p\u003e","tags":[],"title":"Building Models"},{"content":"","date":"0001-01-01","id":25,"permalink":"/docs/maintenance/","summary":"","tags":[],"title":"Maintenance"},{"content":"","date":"0001-01-01","id":26,"permalink":"/blog/","summary":"","tags":[],"title":"Blog"},{"content":"","date":"0001-01-01","id":27,"permalink":"/categories/","summary":"","tags":[],"title":"Categories"},{"content":"","date":"0001-01-01","id":28,"permalink":"/contributors/","summary":"","tags":[],"title":"Contributors"},{"content":"","date":"0001-01-01","id":29,"permalink":"/","summary":"","tags":[],"title":"CQRS \u0026 Event Sourcing for PHP"},{"content":"","date":"0001-01-01","id":30,"permalink":"/docs/","summary":"","tags":[],"title":"Documentation"},{"content":"","date":"0001-01-01","id":31,"permalink":"/tags/","summary":"","tags":[],"title":"Tags"}]