phpgears / event-sourcing
Event Sourcing base
Requires
- php: ^7.1
- phpgears/aggregate: ~0.2.1
- phpgears/event: ~0.3.3
Requires (Dev)
- brainmaestro/composer-git-hooks: ^2.8
- friendsofphp/php-cs-fixer: ^2.16
- infection/infection: ^0.13|^0.15
- overtrue/phplint: ^1.2
- phpmd/phpmd: ^2.8
- phpstan/extension-installer: ^1.0.3
- phpstan/phpstan: ^0.12
- phpstan/phpstan-deprecation-rules: ^0.12
- phpstan/phpstan-strict-rules: ^0.12
- phpunit/phpunit: ^7.5|^8.0
- povils/phpmnd: ^2.1
- roave/security-advisories: dev-master
- sebastian/phpcpd: ^4.0
- squizlabs/php_codesniffer: ^3.5
- thecodingmachine/phpstan-strict-rules: ^0.12
This package is auto-updated.
Last update: 2024-12-13 07:26:28 UTC
README
Event Sourcing
Event Sourcing base
Installation
Composer
composer require phpgears/event-sourcing
Usage
Require composer autoload file
require './vendor/autoload.php';
Aggregate identity
Aggregate identities are provided by gears/identity, head over there to learn about them
Aggregate events
Aggregate Events must implement Gears\EventSourcing\Event\AggregateEvent
interface so they can be used by Aggregate roots. You can extend from Gears\EventSourcing\Event\AbstractAggregateEvent
for simplicity
use Gears\EventSourcing\Event\AbstractAggregateEvent; use Gears\Identity\Identity; class AggregateCreated extends AbstractAggregateEvent { public static function instantiate(Identity $aggregateId) { return self::occurred($aggregateId, []); } } /** * @method getThing(): string */ class SomethingHappened extends AbstractAggregateEvent { public static function hasHappened(Identity $aggregateId, string $whatHappened) { return self::occurred($aggregateId, ['thing' => $whatHappened]); } } /** * @method getReason(): string */ class SomethingFinished extends AbstractAggregateEvent { public static function hasFinished(Identity $aggregateId, string $reason) { return self::occurred($aggregateId, ['reason' => $reason]); } }
Mind that AbstractAggregateEvent constructor is protected forcing you to create static named constructors methods so you can take advantage of type hinting for payload. Although you could use protected method to create aggregate events you're encouraged to use occurred
method
This events are then recorded and applied on Aggregate root
Aggregate root
Aggregate root should implement Gears\EventSourcing\Aggregate\AggregateRoot
interface. You can extend from Gears\EventSourcing\Aggregate\AbstractAggregateRoot
for simplicity
use Gears\EventSourcing\Aggregate\AbstractAggregateRoot; use Gears\Identity\Identity; class CustomAggregate extends AbstractAggregateRoot { public static function create(Identity $identity): self { $instance = new self(); $instance->recordAggregateEvent(AggregateCreated::instantiate($identity)); return $instance; } public function doSomething(): void { $this->recordAggregateEvent(SomethingHappened::hasHappened($this->getIdentity(), 'this happened')); } public function finishSomething(): void { $this->recordAggregateEvent(SomethingFinished::hasFinished($this->getIdentity(), 'this finished')); $this->recordEvent(WhateverFinished::instance()); } protected function applyAggregateCreated(AggregateCreated $event): void { $this->setIdentity($event->getAggregateId()); } protected function applySomethingHappened(SomethingHappened $event): void { // do something with $event->getThing(); } protected function applySomethingFinished(SomethingFinished $event): void { // do something with $event->getReason(); } }
Aggregate operations
Every operation in aggregates should be made through aggregate events, even aggregate's own creation (see example above), that's the reason AbstractAggregateRoot constructor is protected.
Aggregate events represent facts relevant to the Event Sourcing system such as AggregateCreated, SomethingHappened and SomethingFinished in the previous example
Aggregate events should be finally collected, persisted on an event store and eventually dispatched to an event bus depending on the Projection mechanism at work
$aggregateId = CustomAggregateIdentity::fromString('4c4316cb-b48b-44fb-a034-90d789966bac'); $customAggregate = CustomAggregate::create($aggregateId); $customAggregate->doSomething(); $eventStore->store($customAggregate->collectRecordedAggregateEvents());
Aggregate Events vs Domain Events
This is a key difference between this package and other Event Sourcing libraries out there
Aggregate roots can collect two fundamentally different types of events, Aggregate events have already been discussed in the section above, the second kind of events are Domain events which represent facts relevant to the Domain
They differ drastically and conceptually on who are this events relevant or meant to. While Aggregate Events are meant only for the Event Sourcing system, that is Event Store persistence, Aggregate root reconstitution, Projection, Snapshotting, Sagas and others, Domain Events are relevant to the application Domain in itself, that is this and/or other parts or Bounded Contexts of your system
You should NEVER use Aggregate events to drive your application execution or signal other parts of your system, You MUST use Domain events for those purposes
Domain events must be collected and sent to an event bus, head to gears/event to learn more about this topic
foreach ($customAggregate->collectRecordedEvents() as $domainEvent) { /** @var \Gears\Event\EventBus $eventBus */ $eventBus->dispatch($domainEvent); }
Event granularity
Granularity is key when dealing with this two kinds of events. As a rule of thumb you can consider that Aggregate Events can be mapped to atomic actions that can be performed on an aggregate root, while Domain events can be seen as actions that a user can perform on the domain, they will naturally have a 1:1 relationship in most cases, but not necessarily in all cases
An example that can be of some help to understand this difference is a two step process of user sign up in a system with email validation
While on a first step a user is created in the system (data is fully or partially collected) the user cannot be considered completely registered until he validates his email on a second step
You can consider two methods on the User aggregate to accomplish this task, createUser() that creates the user Aggregate Root with whatever data provided, and validateUser() which validates the user based for example on a code sent to his email
Both of these methods will create an Aggregate Event that will be eventually persisted onto the Event Store, but only the later will create a UserRegistered domain event which can be relevant for other parts of your system (e.g. creating a user wallet on the billing Bounded Context)
Aggregate Repository
Event Store
Concurrency
Snapshot Store
Contributing
Found a bug or have a feature request? Please open a new issue. Have a look at existing issues before.
See file CONTRIBUTING.md
License
See file LICENSE included with the source code for a copy of the license terms.