lidskasila / glow
Simple [C]ommand [Q]uery [R]esponsibility [S]egregation and [E]vent [S]ourcing library.
Requires
- php: >=7.1
- ramsey/uuid: ^3.6
- roave/security-advisories: dev-master
Requires (Dev)
- mockery/mockery: ^0.9
- phing/phing: ^2.16
- phpunit/phpunit: ^6.2
This package is not auto-updated.
Last update: 2024-10-27 02:18:19 UTC
README
This library is to show another way to tackle CQRS and event sourcing, but is not considered as suitable for real production projects. For this purpose rather use Prooph toolbox or Nette extension for Prooph toolbox. It supports snapshoting, read model projections, is easy to make event replaying and is way more flexible.This PHP CQRS EventSourcing library is based on Benjamin Eberlei's LiteCQRS for php which was not maintained for quite a long time. This fork is bringing it back to life, but it is not to be considered as a stable library, since there may be BC breaks in the near future.
Main differences are:
- Minimal required version of PHP is 7.0, it will be 7.1 soon.
- Commanding - to the CommandBus you can register only implementations of ComandHandler (has only handle method). The reason behind this is to enforce explicitness in naming and structuring conventions. When you see XxxCommand and you need to see the implementation, you know there should be XxxCommandHandler implemented somewhere.
Original updated readme
Small naming-convention based CQRS library for PHP (loosely based on LiteCQRS for C#) that relies on the MessageBus, Command, EventSourcing and Domain Event patterns.
Terminology
CQS is Command-Query-Separation: A paradigm where read methods never change state and write methods never return data. Build on top, CQRS suggests the separation of read- from write-model and uses the DomainEvent pattern to notify the read model about changes in the write model.
Glow uses the command pattern and a central message bus service that
finds the corresponding handler to execute a command. A command must implement
Glow\Commanding\Command
. It should be a plain DTO (data transfer object)
with just some properties describing it (imutability is encouraged).
After this object is passed as a parameter into the CommandBus' handle(Command $command)
method, CommandBus finds a proper CommandHandler and invokes the
same method with the same parameter on it. Enforced convention is that there
has to be XxxCommandHandler (implementing CommandHandler) for every XxxCommand.
During the execution of a command, domain events can be triggered. These are
again just simple classes with some properties and they can optionally implement
Glow\DomainEvent
.
An event queue knows what domain events have been triggered during a command and then publishes them to an event message bus, where many listeners can listen to them.
Changes
Conventions
- Each XxxCommand DTO is mapped to XxxCommandHandler when XxxCommand implements Command and XxxCommandHandler implements CommandHandler.
- Domain Events are applied to Event Handlers "Event Class Shortname" => "onEventClassShortname". An event listener is registered only if this matches.
- Domain Events are applied on Entities/Aggregate Roots "Event Class Shortname" => "applyEventClassShortname"
- You can optionally extend the
DefaultDomainEvent
which has a constructor that maps its array input to properties and throws an exception if an unknown property is passed. - There is also a
DefaultCommand
with the same semantics asDefaultDomainEvent
. Extending this is not required.
Examples:
GreetingCommand implements Command
maps to thehandle(GreetingCommand $command)
method on the registered GreetingCommandHandler implementing CommandHandler.HelloWorld\GreetedEvent
is passed to all event handlers that have a methodonGreeted(GreetedEvent $event)
.HelloWorld\Events\Greeted
is passed to all event handlers that have a methodonGreeted(Greeted $event)
.HelloWorld\GreetedEvent
is delegated toapplyGreeted($event)
when created on the aggregate root
Installation & Requirements
Install with Composer:
composer require lidskasila/glow
Workflow
These are the steps that a command regularly takes through the Glow stack during execution:
- You push commands into a
CommandBus
. Commands are simple objects implementingCommand
created by you. - The
CommandBus
checks for a handler that can execute your command. Every command has exactly one handler. - The command handler changes state of the domain model. It does so by
creating events (that represent a state change) and passing them to the
AggregateRoot::apply()
orDomainEventProvider::raise()
method of your domain objects. - When the command is completed, the command bus will check all objects in the identity map for events.
- All found events will be passed to the
EventMessageBus#publish()
method. - The EventMessageBus dispatches all events to observing event handlers.
- Event Handlers can create new commands again using the
CommandBus
.
Command and Event handler execution can be wrapped in handlers that manage transactions. Event handling is always triggered outside of any command transaction. If the command fails with any exception all events created by the command are forgotten/ignored. No event handlers will be triggered in this case.
In the case of InMemory CommandBus and EventMessageBus Glow makes sure that the execution of commands and event handlers is never nested, but in sequential linearized order. This prevents independent transactions for each command from affecting each other.
#TODO following is not updated
Examples
See examples/ for some examples:
example1.php
shows usage of the Command- and EventMessageBus with one domain objectexample2_event.php
shows direct usage of the EventMessageBus inside a commandexample3_sequential_commands.php
demonstrates how commands are processed sequentially.tictactoe.php
implements a tic tac toe game with CQRS.SymfonyExample.md
showsexample1.php
implemented within the scope of a Symfony2 project.
Setup
- In Memory Command Handlers, no event publishing/observing
<?php $userService = new UserService(); $commandBus = new DirectCommandBus() $commandBus->register('MyApp\ChangeEmailCommand', $userService);
- In Memory Commands and Events Handlers
This uses Glow\EventProviderInterface
instances to trigger domain events.
<?php // 1. Setup the Library with InMemory Handlers $messageBus = new InMemoryEventMessageBus(); $identityMap = new SimpleIdentityMap(); $queue = new EventProviderQueue($identityMap); $commandBus = new DirectCommandBus(array( new EventMessageHandlerFactory($messageBus, $queue) )); // 2. Register a command service and an event handler $userService = new UserService($identityMap); $commandBus->register('MyApp\ChangeEmailCommand', $userService); $someEventHandler = new MyEventHandler(); $messageBus->register($someEventHandler);
- In Memory Commands + Custom Event Queue
Glow knows about triggered events by asking Glow\Bus\EventQueue
.
Provide your own implementation to be independent of
your domain objects having to implement EventProviderInterface
.
<?php $messageBus = new InMemoryEventMessageBus(); $queue = new MyCustomEventQueue(); $commandBus = new DirectCommandBus(array( new EventMessageHandlerFactory($messageBus, $queue) ));
Usage
To implement a Use Case of your application
- Create a command object that receives all the necessary input values. Use public properties and extend
Glow\DefaultCommand
to simplify. - Add a new method with the name of the command to any of your services (command handler)
- Register the command handler to handle the given command on the CommandBus.
- Have your entities implement
Glow\AggregateRoot
orGlow\DomainEventProvider
- Use protected method
raise(DomainEvent $event)
or apply(DomainEvent $event)`` to attach events to your aggregate root objects.
That is all there is for simple use-cases.
If your command triggers events that listeners check for, you should:
- Create a domain specific event class. Use public properties to simplify.
- Create a event handler(s) or add method(s) to existing event handler(s).
While it seems "complicated" to create commands and events for every use-case. These objects are really dumb and only contain public properties. Using your IDE or editor functionality you can easily generate them in no time. In turn, they will make your code very explicit.
Difference between apply() and raise()
There are two ways to publish events to the outside world.
DomainEventProvider#raise(DomainEvent $event)
is the simple one, it emits an event and does nothing more.AggregateRoot#apply(DomainEvent $event)
requires you to add a methodapply$eventName($event)
that can be used to replay events on objects. This is used to replay an object from events.
If you don't use event sourcing then you are fine just using raise()
and ignoring apply()
altogether.
Failing Events
The EventMessageBus prevents exceptions from bubbling up. To allow some debugging of failed event handler
execution there is a special event "EventExecutionFailed" that you can listen to. You will get passed
an instance of Glow\Bus\EventExecutionFailed
with properties $exception
, $service
and
$event
to allow analysing failures in your application.
Extension Points
You should implement your own CommandBus
or extend the existing to wire the whole process together
exactly as you need it to work.
Plugins
Symfony
Inside symfony you can use Glow by registering services with
lite_cqrs.command_handler
or the lite_cqrs.event_handler
tag. These
services are then autodiscovered for commands and events.
Command- and Event-Handlers are lazily loaded from the Symfony Dependency Injection Container.
To enable the bundle put the following in your Kernel:
new \Glow\Plugin\SymfonyBundle\GlowBundle(),
You can enable/disable the bundle by adding the following to your config.yml:
lite_cqrs: ~
Please refer to the SymfonyExample.md document for a full demonstration of using Glow from within a Symfony2 project.
Monolog
A plugin that logs the execution of every command and handler using Monolog. It includes the type and name of the message, its parameters as json and if its execution succeeded or failed.
The Monolog integration into Symfony registers a specific channel lite_cqrs
which you can configure differently from the default channels in Symfony. See
the Symfony
cookbook
for more information.