coajaxial/messaging-mediator

Send messages directly from your domain model without any dependencies

dev-master 2020-10-27 08:02 UTC

This package is auto-updated.

Last update: 2024-03-27 16:00:57 UTC


README

Messaging Mediator

Psalm Unit tests Integration tests Latest Stable Version Total Downloads License

The messaging mediator hooks into your message bus, giving you the ability to yield messages from your application and domain layer, including message handlers, aggregates, value objects, domain services, etc.

Publish domain events, dispatch commands and issue queries and get their results without any dependency to your message bus.

Installation

⚠️ This library has no stable release! It currently only provides a middleware for Symfony's messenger component and testing aids for PHPUnit.

composer require coajaxial/messaging-mediator:@dev

Next, you need to configure your message bus to use the mediator.

Symfony messenger component (manually)

<?php

use Coajaxial\MessagingMediator\Adapter\Messenger\MessageBusAdapter;
use Coajaxial\MessagingMediator\Adapter\Messenger\MessagingMediatorMiddleware;
use Coajaxial\MessagingMediator\MessagingMediator;
use Coajaxial\MessagingMediator\Testing\LazyMessageBus;
use Symfony\Component\Messenger\Handler\HandlersLocator;
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;

$mediatorBus = new LazyMessageBus();

$mediator = new MessagingMediator($mediatorBus);

$bus = new MessageBus(
    [
        // The messaging mediator middleware should be right 
        // before the handle message middleware
        new MessagingMediatorMiddleware($mediator),
        new HandleMessageMiddleware(
            new HandlersLocator(
                [
                    // Your handler configuration
                ]
            )
        ),
    ]
);

$mediatorBus->initialize(new MessageBusAdapter($bus));

// $bus->dispatch(new MyMessage());

Use cases

Publish domain events

Publish domain events from your aggregates by yielding domain event instances.

<?php 

class MyAggregate 
{
    public static function init(): Generator 
    {
        yield new MyAggregateInited();

        return new self();
    }
}

class MyHandler 
{
    public function __invoke(MyCommand $command): Generator 
    {
        $agg = yield from MyAggregate::init();
    }
}

Execute commands

This is useful for domain event subscribers or long running processes (sagas). Just yield a command instance and you are done.

<?php

/**
 * Give the user 100 starting credits when he signs up before 2020-01-01
 */
class StartingCreditListener 
{
    public function __invoke(UserSignedUp $event): Generator 
    {
        if ( $event->getPublishedAt() < DateTimeImmutable::createFromFormat('Y-m-d', '2021-01-01') ) {
            yield new ChargeAccount($event->getUserId(), 100);
        }       
    }
}

ℹ️ You can use try ... catch around the yield statement to catch exceptions happening during command execution.

Issue queries to enforce soft business rules

You can issue queries and get it's result to enforce some business constraints that don't need to be transactional consistent. Just yield a query object and the mediator will send the result back to the Generator.

<?php

class Post 
{
    public function publish(): Generator 
    {
        $numberOfPublishedPostsToday = yield new NumberOfPublishedPostsTodayByAuthor($this->authorId);

        if ( $numberOfPublishedPostsToday >= 3 ) {
            throw new DomainException('Number of maximum posts per day reached.');
        }
    }
}

class PublishPostHandler 
{
    public function __invoke(MyCommand $command): Generator 
    {
        $post = new Post(); // Usually from the repository

        yield from $post->publish();
    }
}

⚠️ Be absolutely sure you are enforcing a soft business rule!

Queries are usually eventual consistent, so the result may not be 100% true by the time issuing the query.

In the example above, domain experts are ok with the fact that there may be more than 3 published posts per author and day in some (negligible) circumstances.

Psalm support

To enable type detection for yielded query results, just add this to your query classes:

/**
 * This query returns the current date.
 *
 * @psalm-template void
 * @psalm-yield DateTimeImmutable
 */
final class Now {
}

class MyAggregate {
    public function test(): Generator {
        $now = yield Now();
        // Psalm now knows that $now is a DateTimeImmutable
    }
}

Motivation

When I first implemented domain events for my domain model, I stored all events in a collection, that could be retrieved and cleared. It looked something like this:

<?php

class MyAggregate {
    /** @var object[] */
    private $events = [];

    public function doSomething(): void {
        // ...
        $this->events = new SomethingHappend();
    }
    
    /** @return object[] */
    public function getEvents(): array {
        return $this->events;
    }

    public function clearEvents(): void {
        $this->events = [];
    }
}

This works quite well, but every aggregate needed this boilerplate, so I had to put this boilerplate either into a super-class, which all of my aggregates extended, or into a trait.

There are some other techniques to implement domain events, for example you can return them, like so:

<?php

class MyAggregate {
    public function doSomething(): array {
        // ...
        return [
            new SomethingHappened()
        ];       
    }   
}

With this approach you have no more boilerplate, but you also loose the ability to return normal values, like for example the self instance for named constructors, or some calculated values.

So this project is my solution to do it, and has several advantages:

  • Absolutely no boilerplate, no need to extend a super-class or use a trait
  • You can still return normal values
  • You can use this method to do any kind of messaging (commands and queries)

Contribute

Build docker image

docker build -t coajaxial/messaging-mediator .

Load shell aliases

There is a shell aliases file that you can source to import some useful aliases, e.g. composer running from within the docker container (coajaxial/messaging-mediator)

source .aliases

Tests