This is a non-obtrusive CQRS + Event Sourcing library that helps building complex DDD web applications.
No inheritance! Your domain code remains clean and infrastructure/framework agnostic as should be.
-
\Dudulina\Event
for each domain event; no methods, it is just a marker interface; the domain events need to be detected by the automated code generation tools; -
\Dudulina\Command
for each domain command; only one method,getAggregateId()
; it is needed by the command dispatcher to know that Aggregate instance to load from Repository -
\Dudulina\ReadModel\ReadModelInterface
for each read model; this is required only if you use theReadModelRecreator
to rebuild your read-models (projections)
Even if only a few interfaces need to be implemented, you could loose the coupling to the library even more. You could define and use your own domain interfaces and only that interfaces would inherit from the library interfaces. In this way, when you change the library, you change only those interfaces.
On the write side, you only need to instantiate a command and send it to the CommandDispatcher
;
Let's create a command.
// immutable and Plain PHP Object (Value Object)
// No inheritance!
class DoSomethingImportantCommand implements Command
{
private $idOfTheAggregate;
private $someDataInTheCommand;
public function __construct($idOfTheAggregate, $someDataInTheCommand)
{
$this->idOfTheAggregate = $this->idOfTheAggregate;
$this->someDataInTheCommand = $this->someDataInTheCommand;
}
public function getAggregateId()
{
return $this->idOfTheAggregate;
}
public function getSomeDataInTheCommand()
{
return $this->someDataInTheCommand;
}
}
Now, let's create a simple event:
// immutable, simple object, no inheritance, minimum dependency
class SomethingImportantHappened implements Event
{
public function __construct($someDataInTheEvent)
{
$this->someDataInTheEvent = $someDataInTheEvent;
}
public function getSomeDataInTheEvent()
{
return $this->someDataInTheEvent;
}
}
Somewhere in the UI or Application layer:
class SomeHttpAction
{
public function getDoSomethingImportant(RequestInterface $request)
{
$idOfTheAggregate = $request->getParsedBody()['id'];
$someDataInTheCommand = $request->getParsedBody()['data'];
$this->commandDispatcher->dispatchCommand(new DoSomethingImportantCommand(
$idOfTheAggregate,
$someDataInTheCommand
));
return new JsonResponse([
'success' => 1,
]);
}
}
That's it. No transaction management, no loading from the repository, nothing. The command arrives to the aggregate's command handler, as an argument, like this:
class OurAggregate
{
//....
public function handleDoSomethingImportant(DoSomethingImportantCommand $command)
{
if($this->ourStateDoesNotPermitThis()){
throw new \Exception("No no, it is not possible!");
}
yield new SomethingImportantHappened($command->getSomeDataInTheCommand());
}
public function applySomethingImportantHappened(SomethingImportantHappened $event, Metadata $metadata)
{
//Metadata is optional
$this->someNewState = $event->someDataInTheEvent;
}
}
The read models receive the raised event to. They process the event after it is persisted. Take a look at a possible read model:
class SomeReadModel
{
//...some database initialization, i.e. a MongoDB database injected in the constructor
public function onSomethingImportantHappened(SomethingImportantHappened $event, Metadata $metadata)
{
$this->database->getCollection('ourReadModel')->insertOne([
'_id' => $metadata->getAggregateId()
'someData' => $event->getSomeDataInTheEvent()
]);
}
//this method could be used by the UI to display the data
public function getSomeDataById($id)
{
$document = $this->database->getCollection('ourReadModel')->findOne([
'_id' => $metadata->getAggregateId()
]);
return $document ? $document['someData'] : null;
}
}
The Read-models can be updated in a separate process, in realtime-like (by tailing) or by polling the Event store or even using JavaScript. Read more here about how you can keep the Read-model up-to-date.
So, when a command is dispatched the following things happen:
- the aggregate class is identified
- the aggregate is loaded from the repository, replaying all previous events
- the command is dispatched to the aggregate instance
- the aggregate yields the events
- the events are persisted to the event store
- the read-models are notified about the new events
- the sagas are notified also; if the sagas generate other commands, the loop is started again.
If an exception is thrown by the command handler on the aggregate, no events are persisted and the exception reach the caller
Read the entire documentation here
There is a MongoDB implementation of the Event store and a Restful HTTP API for this Event store if you want to build Read-models in any other languages than PHP.
A JavaScript connector is also available. Here you can find some examples of updating a Read-model in JavaScript.
The library can dispatch queries also. The Askers ask questions and the Answerers answser them.
The Askers ask question to the \Dudulina\Query\Asker
and the can receive the answer as return value or as callback on them
(the method $this->whenAnsweredXYZ
or the one marked with @QueryAsker
).
The Answerers answer questions at the $this->whenAskedXXX
or @QueryHandler
marked methods.
They can also answer a question when they know that the answer has changed and all the askers are notified, by calling \Dudulina\Query\Answerer::answer()
.
How does the library know what command handler to call when a command is dispatched? Or what read models to notify when a new event is published? The answer to all these questions is CQRS bindings.
Long story short, the tools analyze the domain code, detect the handlers and build a PHP file with all the bindings as classes.
Then you use those classes to configure the CommandDispatcher. The create_bindings.php
must be run every time the domain code changes.
php -f vendor/xprt64/dudulina/bin/create_bindings.php -- --src="/some/source/directory" --src="/some/other/source/directory" > cqrs_bindings.php
Then you need to include the file create_bindings.php
to your index.php
usually after vendors/autoload.php
.
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../deploy/cqrs_bindings.php';
A Todo list sample application can be found at github.com/xprt64/todosample-cqrs-es.
Read more about how to query an Aggregate in order to test if a command will succeed or not, without actually executing it.
Feel free to post to this group: https://groups.google.com/forum/#!forum/cqrs--event-sourcing-for-php.