star/domain-event

Domain events extension

2.5.2 2024-01-21 17:59 UTC

This package is auto-updated.

Last update: 2024-04-21 18:31:13 UTC


README

Build Status

Small implementation of the aggregate root in ddd. The AggregateRoot implementation triggers events that can be collected for publishing by an implementation of EventPublisher.

Installation

  • Add the package using composer in your composer.json

composer require star/domain-event

Usage

  1. Make your class inherit the AggregateRoot class.
// Product.php
class Product extends AggregateRoot
{
}
  1. Create the event for the mutation.
class ProductWasCreated implements DomainEvent
{
    private $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function name(): string
    {
        return $this->name;
    }
}
  1. Create a named constructor (static method), or a mutation method.
// Product.php
    /**
     * Static construct, since the __construct() is protected
     *
     * @param string $name
     * @return Product
     */
    public static function draftProduct(string $name): Product
    {
        return self::fromStream([new ProductWasCreated($name)]);
    }
    
    /**
     * Mutation method that handles the business logic of your aggregate
     */
    public function confirm(): void
    {
        $this->mutate(new ProductWasConfirmed($this->getId()));
    }
  1. Create the callback method on the AggregateRoot that would be used to set the state after an event mutation.
    protected function onProductWasCreated(ProductWasCreated $event): void
    {
        $this->name = $event->name();
    }
}

Listening to an event

When you wish to perform an operation after an event was dispatched by the EventPublisher, you need to define your listener:

class DoSomethingProductCreated implements EventListener
{
    // methods on listener can be anything, it is configured by listensTo
    public function doSomething(ProductWasCreated $event): void
    {
        // do something with the event
    }

    public function doSomethingAtFirst(PostWasPublished $event): void 
    {
    }

    public function doSomethingInBetween(PostWasPublished $event): void 
    {
    }

    public function doSomethingAtLast(PostWasPublished $event): void 
    {
    }
    
    public function listensTo(): array
    {
        return [
            ProductWasCreated::class => 'doSomething', // priority will be assigned at runtime
            PostWasPublished::class => [ // multiple methods may be assigned priority
                100 => 'doSomethingAtFirst',
                0 => 'doSomethingInBetween',
                -20 => 'doSomethingAtLast',
            ],
        ];
    }
}

The listener needs to be given to the publisher, so that he can send the event.

$publisher = new class() implements EventPublisher {}; // your implementation choice
$publisher->subscribe(new DoSomethingProductCreated()); // This is a subscriber that listens to the ProductWasCreated event

$product = Product::draftProduct('lightsaber');
$publisher->publishChanges($product->uncommitedEvents()); // will notify the listener and call the DoSomethingProductCreated::doSomething() method

Warning: Be advised that events will be removed from aggregate once collected, to avoid republishing the same event twice.

We currently support third party adapters to allow you to plug-in the library into your infrastructure.

Naming standard

The events method on the AggregateRoot children must be prefixed with on and followed by the event name. ie. For an event class named StuffWasDone the aggregate should have a method:

protected function onStuffWasDone(StuffWasDone $event): void;

Note: The callback method can be changed to another format, by overriding the AggregateRoot::getEventMethod().

Message bus

The package adds the ability to dispatch messages (Command and Query). Compared to the EventPubliser, the CommandBus and QueryBus have different usages.

  • Command bus: Responsible to dispatch an operation that returns nothing.
  • Query bus: Responsible to fetch some information. The returned information is recommended to be returned in a readonly format.

(Example of usage)

Example

The blog example shows a use case for a blog application.

Symfony usage

Using a Symfony application, you may use the provided compiler passes to use the buses.

use Star\Component\DomainEvent\Ports\Symfony\DependencyInjection\CommandBusPass;
use Star\Component\DomainEvent\Ports\Symfony\DependencyInjection\EventPublisherPass;
use Star\Component\DomainEvent\Ports\Symfony\DependencyInjection\QueryBusPass;

// Kernel.php
public function build(ContainerBuilder $container): void {
    $container->addCompilerPass(new CommandBusPass());
    $container->addCompilerPass(new QueryBusPass());
    $container->addCompilerPass(new EventPublisherPass());
}

Once registered, three new tags will be available:

  • star.command_handler
  • star.query_handler
  • star.event_publisher

The tags star.command_handler and star.query_handler have an optional attribute message to specify the message FQCN that will be mapped to this handler. By default the system will try to resolve the same FQCN as the handler, without the Handler suffix.

// services.yaml
services:
    Path/For/My/Project/DoStuffHandler:
      tags:
        - { name star.command_handler, message: Path/For/My/Project/DoStuff }

    Path/For/My/Project/FetchStuffHandler:
      tags:
        - { name star.query_handler, message: Path\For\My\Project\FetchStuff }

Note: In both cases, omitting the message attributes would result in the same behavior.

Event store

Event stores are where your events will be persisted. You define which platform is used by extending the provided store.

Example with DBALEventStore.