Skip to content

Latest commit

 

History

History
206 lines (154 loc) · 7.14 KB

domain-events.md

File metadata and controls

206 lines (154 loc) · 7.14 KB

Domain Events

Domain events are part of the main AggregateRoot class. Extend this to be able to raise events in your entity.

Raising Events

To raise an event, decide which actions should result in a domain event. These should coincide with state changes in the domain objects and the events should originate from your main entities (aggregate roots).

For example: you may want to raise an event when a new User entity is created or that a role was added to the user.

This does necessitate some changes to how you typically work with entities and Doctrine in that you should remove setters and nullable constructor arguments; instead you will need to manage changes to your entity through specific methods, for example:

  • completeOrder()
  • updatePermissions()
  • revokePermissions()
  • publishStory()

Internally, after updating the entity state, call: $this->raise(NameOfEvent::class, []) and pass any specific parameters into the event that you want to make available to the listener. This could be the old vs new or the entire entity reference, it is entirely up to you.

<?php
use Somnambulist\Components\Models\AggregateRoot;

class SomeObject extends AggregateRoot
{
    public function __construct($id, $name, $another, $createdAt)
    {
        $this->id        = $id;
        $this->name      = $name;
        $this->another   = $another;
        $this->createdAt = $createdAt;
        
        $this->raise(MyEntityCreatedEvent::class, ['id' => $id, 'name' => $name, 'another' => $another]);
    }
}

Generally it is better to not raise events in the constructor but instead to use named constructors for primary object creation:

<?php
use Somnambulist\Components\Models\AggregateRoot;

class SomeObject extends AggregateRoot
{
    private function __construct($id, $name, $another, $createdAt)
    {
        $this->id        = $id;
        $this->name      = $name;
        $this->another   = $another;
        $this->createdAt = $createdAt;
    }
    
    public static function create($id, $name, $another)
    {
        $entity = new static($id, $name, $another, new DateTime());
        $entity->raise(MyEntityCreatedEvent::class, ['id' => $id, 'name' => $name, 'another' => $another]);
        
        return $entity;
    }
}

Defining an Event

To define your own event extend the AbstractDomainEvent object. That's basically it!

<?php
use Somnambulist\Components\Events\AbstractEvent;

class MyEntityCreatedEvent extends AbstractEvent
{

}

You can create an intermediary to add base methods to your events e.g.: if you want to broadcast through a message queue a generic snake_case name using the group and event class name is generated by default. You may wish to change this by overriding the behaviour:

<?php
use Somnambulist\Components\Events\AbstractEvent;

abstract class AppDomainEvent extends AbstractEvent
{
    protected string $group = 'app';

    public function longName(): string
    {
        return sprintf('%s.%s', $this->group(), strtolower($this->name()));
    }
}

And then extend it with the overrides you need:

<?php
class MyEntityCreatedEvent extends AppDomainEvent
{

}

Notifying Domain Events

Doctrine Integration

This implementation includes a Doctrine subscriber that will listen for AggregateRoots. These are collected and once the Unit of Work has been flushed successfully will be dispatched via the EventBus implementation that is in use (default Messenger).

Note: it is not required to use the DomainEventPublisher subscriber. You can implement your own event dispatcher, use another dispatcher entirely (the frameworks) and then manually trigger the domain events by flushing the changes and then manually calling releaseAndResetEvents and dispatching the events.

To use the included listener, add it to your list of event subscribers in the Doctrine configuration. This is per entity manager.

Note: to use listeners with domain events that rely on Doctrine repositories it is necessary to defer loading those subscribers until after Doctrine has been resolved.

As of v3 the events are only broadcast on the event bus and are not sent to Doctrines event manager.

Messenger Integration

This dispatcher allows you to register aggregates roots with the dispatcher, and then once you have finished manipulating the domain; notify all event changes to the bound event bus (default Messenger).

This dispatcher uses an abstract base that includes methods for sorting and collecting the events. It can be extended to perform other tasks.

This dispatcher can be registered with the kernel.terminate event so that any collected events are fired at the end of the current request.

Remember: you will need to register the objects that will raise events.

Note: the Messenger dispatcher does not release monitored aggregates after event dispatch. You will need to specifically stop listening for events to clear the listener.

Be sure to read the posts by Benjamin Eberlei mentioned earlier and check out his Assertion library for low dependency entity validation.

See messenger docs for more details.

Decorating Events

When events are dispatched, they may require additional contextual information be provided by the system performing the dispatch. Typically: this arises because the events are generated with the domain objects, so they are not aware of e.g. the current user, or system, making them.

Event decorators can be added and bound to the event adapters that can add this metadata to the events before dispatch. The decorators can use whatever services are necessary to tag the events. For example: adding a unique request id to the event to track processing through a micro-services system:

class DecorateWithRequestId implements EventDecoratorInterface
{
    private string $header = 'X-Request-Id';

    public function __construct(private RequestStack $requestStack, string $header = null)
    {
        if (!is_null($header)) {
            $this->header = $header;
        }
    }

    public function decorate(AbstractEvent $event): AbstractEvent
    {
        $request = $this->requestStack->getCurrentRequest();

        return $event->appendContext(['request_id' => $request->headers->get($this->header)]);
    }
}

This will add from the current request context a header named X-Request-Id to the event. The id would be generated by the initiating system and then be passed down to all requests made within the system. This allows for collating a request/response cycle across multiple systems for troubleshooting.

Multiple decorators can be added, however they should by tuned to ensure they process as quickly as possible otherwise application performance may suffer as a result. Many events may be raised from a complex domain that is performing multiple changes at once.