cvek / domain-events
Component to add domain events feature
Installs: 382
Dependents: 0
Suggesters: 0
Security: 0
Stars: 2
Watchers: 1
Forks: 1
Open Issues: 0
Type:symfony-bundle
Requires
- php: ^7.4|^8.0
- doctrine/orm: ^2.0|^3.0
- symfony/config: ^4.0|^5.0|^6.0|^7.0
- symfony/dependency-injection: ^4.0|^5.0|^6.0|^7.0
- symfony/doctrine-bridge: ^4.0|^5.0|^6.0|^7.0
- symfony/event-dispatcher: ^4.0|^5.0|^6.0|^7.0
- symfony/http-kernel: ^4.0|^5.0|^6.0|^7.0
- symfony/messenger: ^4.0|^5.0|^6.0|^7.0
Requires (Dev)
- roave/security-advisories: dev-master
README
The bundle integrates with Symfony application to provide domain events support. This allows to be doctrine-agnostic and raise events during the business logic implementation.
Domain events are of three types:
preFlush
. Will be processed synchronously before persisting to DB;onFlush
. Will be processed during persisting to DB (sync or async - see below);postFlush
. Will be processed after persisting to DB (sync or async - see below).
To use this bundle implement RaiseEventsInterface
interface in your entity class and create your custom domain events. We recommend you to use RaiseEventsTrait
to simplify this even more.
Sync/Async messages
Any domain event can be executed in a sync
or an async
way during onFlush
and postFlush
Doctrine events. Extend one of abstract classes: AbstractSyncDomainEvent
or AbstractAsyncDomainEvent
to have sync
or async
event respectively.
Async way is a very powerful approach and must be used in the following cases:
- you want to postpone some time-consuming or remote tasks;
- you want to avoid restrictions of Doctrine event system (e.g. see this and this).
Thanks to db
transport with doctrine storage (will be created automatically) - all the async domain events will be routed and persisted to db automatically.
To consume them we recommend to use the following supervisor config:
[program:db]
command=/srv/api/bin/console messenger:consume db --memory-limit=128M --time-limit=3600 --limit=100
process_name=%(program_name)s_%(process_num)02d
numprocs=1
autostart=true
autorestart=true
startsecs=0
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
⚠️ Dont forget to add it to your application!
Direct Async message
If you dont want to write your own async message handler, but dispatch your message directly, then simply extend AbstractDirectAsyncDomainEvent
in your message and use it this way:
use Doctrine\ORM\Events; public function setName(string $name): self { $this->name = $name; $this->raise(new NameChangedDirectAsyncMessage($this)); return $this; }
The message will be dispatched directly to bus, without of need to write your own handler.
Example
Install
composer req cvek/domain-events
Use
Create domain event
Sync event
use \Cvek\DomainEventsBundle\EventDispatch\Event\AbstractSyncDomainEvent; final class FooNameChanged extends AbstractSyncDomainEvent { private Foo $foo; private string $oldName; private string $newName; public function __construct(Foo $foo, string $oldName, string $newName) { $this->foo = $foo; $this->oldName = $oldName; $this->newName = $newName; } public function getFoo(): Foo { return $this->foo; } public function getOldName(): string { return $this->oldName; } public function getNewName(): string { return $this->newName; } public function isAsync() : bool { return false; } }
Async event
use \Cvek\DomainEventsBundle\EventDispatch\Event\AbstractAsyncDomainEvent; final class FooPasswordChanged extends AbstractAsyncDomainEvent { private Foo $foo; private string $password; public function __construct(Foo $foo, string $password) { $this->foo = $foo; $this->password = $password; } public function getFoo(): Foo { return $this->foo; } public function getPassword(): string { return $this->password; } public function isAsync() : bool { return true; } }
Raise event in your business layer
use \Cvek\DomainEventsBundle\Entity\RaiseEventsInterface; use \Cvek\DomainEventsBundle\Entity\RaiseEventsTrait; class Foo implements RaiseEventsInterface { use RaiseEventsTrait; private string $name; public function setName(string $name): self { $this->raise(new FooNameChanged($this, $this->name, $name)); $this->name = $name; return $this; } public function setPassword(string $password): self { $this->raise(new FooPasswordChanged($this, $password)); return $this; } }
Catch event in listener
When flush
operation will be invoked, all the events, raised in your entities, will be collected and dispatched. You can listen on them in a usual manner.
Listen on sync message
use \Symfony\Component\EventDispatcher\EventSubscriberInterface; use \Doctrine\ORM\Events; final class FooNameListener implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ FooNameChanged::class => 'onNameChange' ]; } public function onNameChange(FooNameChanged $event): void { if ($event->getLifecycleEvent() === Events::preFlush) { // your custom logic on preFlush moment: logging, validation etc... } if ($event->getLifecycleEvent() === Events::onFlush) { // your custom logic on onFlush moment } if ($event->getLifecycleEvent() === Events::postFlush) { // your custom logic on postFlush moment } } }
Listen on async message
use \Doctrine\ORM\Events; use \Symfony\Component\Messenger\Handler\MessageHandlerInterface; final class FooPasswordHandler implements MessageHandlerInterface { public function __invoke(FooPasswordChanged $event) { if ($event->getLifecycleEvent() === Events::preFlush) { // your custom logic on preFlush moment: logging, validation etc... } if ($event->getLifecycleEvent() === Events::onFlush) { // your custom logic on onFlush moment } if ($event->getLifecycleEvent() === Events::postFlush) { // your custom logic on postFlush moment } } }