Part II - The Building Aggregate

In Event Engine we can take a short cut and skip command handlers. This is possible because Aggregates in Event Engine are stateless and pure. This means that they don't have internal state and also no dependencies.

Simply put: they are just functions

Event Engine can take over the boilerplate and we, as developers, can focus on the business logic. I'll explain in greater detail later, but first we want to see a pure aggregate function in action.

Note: If you've worked with a CQRS framework before, it's maybe confusing because normally a command is handled by a command handler (comparable to an application service that handles a domain action) and the command handler would load a business entity or "DDD" aggregate from a repository. We still use the aggregate concept but make use of a functional programming approach. It keeps the domain model lean and testable and allows some nice optimizations for RAD infrastructure.

Let's add the first aggregate called Building in a new Model folder:

<?php

declare(strict_types=1);

namespace MyService\Domain\Model;

use EventEngine\Messaging\Message;

final class Building
{
    public static function add(Message $addBuilding): \Generator
    {
        //yield domain events
    }
}

As you can see the Building class uses static methods. It does not extend from a base class and has no dependencies. We could also use plain PHP functions instead but unfortunately PHP does not provide function autoloading (yet), so we stick to static methods and group all methods of an aggregate in a class.

Building::add() receives AddBuilding messages (of type command) and should perform the business logic needed to add a new building to our application. But instead of adding a new building directly we're ask to yield a domain event.

A neat feature called Flavours allows you to choose between different programming styles. Throughout the tutorial we use the PrototypingFlavour. Two bonus parts at the end of the tutorial introduce the FunctionalFlavour and OopFlavour.

Domain Events

Domain events are the second message type used by Event Engine. The domain model is event sourced, meaning it records all state changes in a series of domain events. These domain events are yielded by aggregate methods and stored in an event store managed by Event Engine. The series of events can then be used to calculate the current state of an aggregate. We will see that in action in a later part of the tutorial and get a better understanding of the technique when we add more use cases.

For now let's add the first domain event in src/Domain/Api/Event:

<?php

declare(strict_types=1);

namespace MyService\Domain\Api;

use EventEngine\EventEngine;
use EventEngine\EventEngineDescription;
use EventEngine\JsonSchema\JsonSchema;

class Event implements EventEngineDescription
{
    const BUILDING_ADDED = 'BuildingAdded';

    /**
     * @param EventEngine $eventEngine
     */
    public static function describe(EventEngine $eventEngine): void
    {
        $eventEngine->registerEvent(
            self::BUILDING_ADDED,
            JsonSchema::object(
                [
                    'buildingId' => JsonSchema::uuid(),
                    'name' => JsonSchema::string(['minLength' => 2])
                ]
            )
        );
    }
}

It looks similar to the AddBuilding command but uses a past tense name. That is a very important difference. Commands tell the application what it should do and events represent facts that have happened.

Yielding Events

Aggregate methods can yield null, one domain event or multiple domain events depending on the result of the executed business logic. If an aggregate method yields null it indicates that no important fact happened and no event needs to be recorded. In many cases an aggregate method will yield one event which is the fact caused by the corresponding command. But there is no one-to-one connection between commands and events. In some cases more than one event is needed to communicate important facts or an error event is yielded instead of the expected event (we'll see that later).

For the first use case we simply yield a BuildingAdded domain event when Building::add() is called with a AddBuilding command.

<?php

declare(strict_types=1);

namespace MyService\Domain\Model;

use MyService\Domain\Api\Event;
use EventEngine\Messaging\Message;

final class Building
{
    public static function add(Message $addBuilding): \Generator
    {
        yield [Event::BUILDING_ADDED, $addBuilding->payload()];
    }
}

The special array syntax for yielding events is a short cut used by Event Engine. It creates the event based on given event name and payload and stores it in the event stream.

Aggregate Description

If we switch back to Cockpit and send the AddBuilding command again, Event Engine still complains about a missing command handler. We need to tell Event Engine about our new aggregate and that it is responsible for handling AddBuilding commands. We can do this in another Event Engine Description in src/Domain/Api/Aggregate.

<?php

declare(strict_types=1);

namespace MyService\Domain\Api;

use MyService\Domain\Model\Building;
use EventEngine\EventEngine;
use EventEngine\EventEngineDescription;

class Aggregate implements EventEngineDescription
{
    const BUILDING = 'Building';

    /**
     * @param EventEngine $eventEngine
     */
    public static function describe(EventEngine $eventEngine): void
    {
        $eventEngine->process(Command::ADD_BUILDING)
            ->withNew(self::BUILDING)
            ->identifiedBy('buildingId')
            ->handle([Building::class, 'add'])
            ->recordThat(Event::BUILDING_ADDED);
    }
}

The connection between command and aggregate is described in a very verbose and readable way. Our IDE can suggest the describing methods of Event Engine's fluent interface and it is easy to remember each step.

  • process tells Event Engine that the following description is for the given command name.
  • withNew/withExisting tells Event Engine which aggregate handles the command and if the aggregate exists already or a new one should be created.
  • identifiedBy tells Event Engine which message payload property should be used to identify the responsible aggregate. Every command sent to the aggregate and every event yielded by the aggregate should contain this property
  • handle takes a callable argument which is the aggregate method responsible for handling the command defined in process. We use the callable array syntax of PHP which can be analyzed by modern IDEs like PHPStorm for auto completion and refactorings.
  • recordThat tells Event Engine which event is yielded by the aggregate's command handling method.

If we try again to send AddBuilding (or reload the schema in Cockpit) we get a new error:

{
  "exception": { 
      "message": "No apply function specified for event: BuildingAdded",
      "details": "..."
    }
}

Command handling works now but an apply function is missing. In part III of the tutorial you'll learn how to add such a function and why it is needed.

Fork me on GitHub