Create events and commands

Last updated at 17 July 2019 | Published at 25 February 2018

Events are the core of any event sourced system. They are the payload, the message, they allow our system to communicate in a meaningful way. Events and commands are very simple objects. They should be modeled as “read-only” objects. This means they have to be instantiated with all the data they need and only expose that data. In EventSauce, they have but one technical requirement:

All events must be objects.

Depending on your serialization strategy your events may need to implement more methods or indicate they implement a certain interface.

Event serialization

In order to persist events they must be serializable. You can create your own serialization strategy, or use the default ones provided.

By default the MessageSerializer uses the PayloadSerializer to serialize events. This serializer requires events to implement the SerializablePayload interface. This interface requires you to implement 2 public functions:

  1. toPayload(): array
  2. fromPayload(array $payload): SerializablePayload

To and From payload

The toPayload and (static) fromPayload methods are used in the serialization process. The toPayload method is expected to return an array that’s serializable as JSON. The fromPayload method is expected to create an instance from a deserialized JSON array.

To illustrate:

$event1 = new MyEvent();
$event2 = MyEvent::fromPayload($event1->toPayload());

assert($event1 == $event2);

Defining events (and commands)

Defining events and commands can be done in 2 ways.

  • Defining them in YAML (code generation).
  • Creating classes by pressing keys on your keyboard.

Manually creating classes.

EventSauce provides interfaces for events and commands. You can create implementations of this. Here are minimal examples.

Event

<?php

use EventSauce\EventSourcing\Serialization\SerializablePayload;

class SomeEvent implements SerializablePayload
{
    public function toPayload(): array
    {
        return ['property' => $this->property];
    }

    public static function fromPayload(array $payload): SerializablePayload
    {
        return new SomeEvent($payload['property']);
    }
}

As you can see in the examples above, there are just 2 required methods. The from and to payload methods are used in the serialization process. This ensures the events can be properly stored. Values returned in the toPayload method should be json_encode-able. Additional required properties for an event should be injected into the constructor and properly formatted in the payload methods.

Defining commands and events using YAML.

Commands and events aren’t very special, they’re often just glorified arrays with accessors. A common name for these kind of objects is “Value Object”. Because of their simplicity it’s possible to use code generation:

<?php

use EventSauce\EventSourcing\CodeGeneration\CodeDumper;
use EventSauce\EventSourcing\CodeGeneration\YamlDefinitionLoader;

$loader = new YamlDefinitionLoader();
$dumper = new CodeDumper();
$phpCode = $dumper->dump($loader->load('path/to/definition.yml'));
file_put_contents($destination, $phpCode);

Here’s an example YAML file containing some command and event definitions.

## The namespace for your generated code.
namespace: Acme\BusinessProcess

## Custom type serialization (optional)
types:
    uuid:
        type: Ramsey\Uuid\UuidInterface
        serializer: |
            {param}->toString()
        deserializer: |
            \Ramsey\Uuid\Uuid::fromString({param})

## The commands (optional)
commands:
    SubscribeToMailingList:
        fields:
            id: uuid
            username:
                type: string
                example: example-user
            mailingList:
                type: string
                example: list-name
    UnsubscribeFromMailingList:
        fields:
            id: uuid
            username:
                type: string
                example: example-user
            mailingList:
                type: string
                example: list-name
            reason:
                type: string
                example: no-longer-interested

## The events
events:
    UserSubscribedToMailingList:
        fields:
            id: uuid
            username:
                type: string
                example: example-user
            mailingList:
                type: string
                example: list-name
    UserUnsubscribedFromMailingList:
        fields:
            id: uuid
            username:
                type: string
                example: example-user
            mailingList:
                type: string
                example: list-name
            reason:
                type: string
                example: no-longer-interested

Which compiles to the following PHP file:

<?php

declare(strict_types=1);

namespace Acme\BusinessProcess;

use EventSauce\EventSourcing\Serialization\SerializablePayload;

final class UserSubscribedToMailingList implements SerializablePayload
{
    /**
     * @var \Ramsey\Uuid\UuidInterface
     */
    private $id;

    /**
     * @var string
     */
    private $username;

    /**
     * @var string
     */
    private $mailingList;

    public function __construct(
        \Ramsey\Uuid\UuidInterface $id,
        string $username,
        string $mailingList
    ) {
        $this->id = $id;
        $this->username = $username;
        $this->mailingList = $mailingList;
    }

    public function id(): \Ramsey\Uuid\UuidInterface
    {
        return $this->id;
    }

    public function username(): string
    {
        return $this->username;
    }

    public function mailingList(): string
    {
        return $this->mailingList;
    }

    public static function fromPayload(array $payload): SerializablePayload
    {
        return new UserSubscribedToMailingList(
            \Ramsey\Uuid\Uuid::fromString($payload['id']),
            (string) $payload['username'],
            (string) $payload['mailingList']
        );
    }

    public function toPayload(): array
    {
        return [
            'id' => $this->id->toString(),
            'username' => (string) $this->username,
            'mailingList' => (string) $this->mailingList,
        ];
    }

    /**
     * @codeCoverageIgnore
     */
    public function withUsername(string $username): UserSubscribedToMailingList
    {
        $clone = clone $this;
        $clone->username = $username;

        return $clone;
    }

    /**
     * @codeCoverageIgnore
     */
    public function withMailingList(string $mailingList): UserSubscribedToMailingList
    {
        $clone = clone $this;
        $clone->mailingList = $mailingList;

        return $clone;
    }

    /**
     * @codeCoverageIgnore
     */
    public static function withId(\Ramsey\Uuid\UuidInterface $id): UserSubscribedToMailingList
    {
        return new UserSubscribedToMailingList(
            $id,
            (string) 'example-user',
            (string) 'list-name'
        );
    }
}

final class UserUnsubscribedFromMailingList implements SerializablePayload
{
    /**
     * @var \Ramsey\Uuid\UuidInterface
     */
    private $id;

    /**
     * @var string
     */
    private $username;

    /**
     * @var string
     */
    private $mailingList;

    /**
     * @var string
     */
    private $reason;

    public function __construct(
        \Ramsey\Uuid\UuidInterface $id,
        string $username,
        string $mailingList,
        string $reason
    ) {
        $this->id = $id;
        $this->username = $username;
        $this->mailingList = $mailingList;
        $this->reason = $reason;
    }

    public function id(): \Ramsey\Uuid\UuidInterface
    {
        return $this->id;
    }

    public function username(): string
    {
        return $this->username;
    }

    public function mailingList(): string
    {
        return $this->mailingList;
    }

    public function reason(): string
    {
        return $this->reason;
    }

    public static function fromPayload(array $payload): SerializablePayload
    {
        return new UserUnsubscribedFromMailingList(
            \Ramsey\Uuid\Uuid::fromString($payload['id']),
            (string) $payload['username'],
            (string) $payload['mailingList'],
            (string) $payload['reason']
        );
    }

    public function toPayload(): array
    {
        return [
            'id' => $this->id->toString(),
            'username' => (string) $this->username,
            'mailingList' => (string) $this->mailingList,
            'reason' => (string) $this->reason,
        ];
    }

    /**
     * @codeCoverageIgnore
     */
    public function withUsername(string $username): UserUnsubscribedFromMailingList
    {
        $clone = clone $this;
        $clone->username = $username;

        return $clone;
    }

    /**
     * @codeCoverageIgnore
     */
    public function withMailingList(string $mailingList): UserUnsubscribedFromMailingList
    {
        $clone = clone $this;
        $clone->mailingList = $mailingList;

        return $clone;
    }

    /**
     * @codeCoverageIgnore
     */
    public function withReason(string $reason): UserUnsubscribedFromMailingList
    {
        $clone = clone $this;
        $clone->reason = $reason;

        return $clone;
    }

    /**
     * @codeCoverageIgnore
     */
    public static function withId(\Ramsey\Uuid\UuidInterface $id): UserUnsubscribedFromMailingList
    {
        return new UserUnsubscribedFromMailingList(
            $id,
            (string) 'example-user',
            (string) 'list-name',
            (string) 'no-longer-interested'
        );
    }
}

final class SubscribeToMailingList implements SerializablePayload
{
    /**
     * @var \Ramsey\Uuid\UuidInterface
     */
    private $id;

    /**
     * @var string
     */
    private $username;

    /**
     * @var string
     */
    private $mailingList;

    public function __construct(
        \Ramsey\Uuid\UuidInterface $id,
        string $username,
        string $mailingList
    ) {
        $this->id = $id;
        $this->username = $username;
        $this->mailingList = $mailingList;
    }

    public function id(): \Ramsey\Uuid\UuidInterface
    {
        return $this->id;
    }

    public function username(): string
    {
        return $this->username;
    }

    public function mailingList(): string
    {
        return $this->mailingList;
    }

    public static function fromPayload(array $payload): SerializablePayload
    {
        return new SubscribeToMailingList(
            \Ramsey\Uuid\Uuid::fromString($payload['id']),
            (string) $payload['username'],
            (string) $payload['mailingList']
        );
    }

    public function toPayload(): array
    {
        return [
            'id' => $this->id->toString(),
            'username' => (string) $this->username,
            'mailingList' => (string) $this->mailingList,
        ];
    }

    /**
     * @codeCoverageIgnore
     */
    public function withUsername(string $username): SubscribeToMailingList
    {
        $clone = clone $this;
        $clone->username = $username;

        return $clone;
    }

    /**
     * @codeCoverageIgnore
     */
    public function withMailingList(string $mailingList): SubscribeToMailingList
    {
        $clone = clone $this;
        $clone->mailingList = $mailingList;

        return $clone;
    }

    /**
     * @codeCoverageIgnore
     */
    public static function withId(\Ramsey\Uuid\UuidInterface $id): SubscribeToMailingList
    {
        return new SubscribeToMailingList(
            $id,
            (string) 'example-user',
            (string) 'list-name'
        );
    }
}

final class UnsubscribeFromMailingList implements SerializablePayload
{
    /**
     * @var \Ramsey\Uuid\UuidInterface
     */
    private $id;

    /**
     * @var string
     */
    private $username;

    /**
     * @var string
     */
    private $mailingList;

    /**
     * @var string
     */
    private $reason;

    public function __construct(
        \Ramsey\Uuid\UuidInterface $id,
        string $username,
        string $mailingList,
        string $reason
    ) {
        $this->id = $id;
        $this->username = $username;
        $this->mailingList = $mailingList;
        $this->reason = $reason;
    }

    public function id(): \Ramsey\Uuid\UuidInterface
    {
        return $this->id;
    }

    public function username(): string
    {
        return $this->username;
    }

    public function mailingList(): string
    {
        return $this->mailingList;
    }

    public function reason(): string
    {
        return $this->reason;
    }

    public static function fromPayload(array $payload): SerializablePayload
    {
        return new UnsubscribeFromMailingList(
            \Ramsey\Uuid\Uuid::fromString($payload['id']),
            (string) $payload['username'],
            (string) $payload['mailingList'],
            (string) $payload['reason']
        );
    }

    public function toPayload(): array
    {
        return [
            'id' => $this->id->toString(),
            'username' => (string) $this->username,
            'mailingList' => (string) $this->mailingList,
            'reason' => (string) $this->reason,
        ];
    }

    /**
     * @codeCoverageIgnore
     */
    public function withUsername(string $username): UnsubscribeFromMailingList
    {
        $clone = clone $this;
        $clone->username = $username;

        return $clone;
    }

    /**
     * @codeCoverageIgnore
     */
    public function withMailingList(string $mailingList): UnsubscribeFromMailingList
    {
        $clone = clone $this;
        $clone->mailingList = $mailingList;

        return $clone;
    }

    /**
     * @codeCoverageIgnore
     */
    public function withReason(string $reason): UnsubscribeFromMailingList
    {
        $clone = clone $this;
        $clone->reason = $reason;

        return $clone;
    }

    /**
     * @codeCoverageIgnore
     */
    public static function withId(\Ramsey\Uuid\UuidInterface $id): UnsubscribeFromMailingList
    {
        return new UnsubscribeFromMailingList(
            $id,
            (string) 'example-user',
            (string) 'list-name',
            (string) 'no-longer-interested'
        );
    }
}