Getting Started

In this guide you analyse a small profile domain and render it as a diagram inspired by Event Storming. You start from an empty PHPStan setup, add the analyser, write a handful of event sourcing classes and end up with a PNG of your domain.

Installation

The analyser is a PHPStan extension, so you install it as a dev dependency next to PHPStan and your event sourcing code:

composer require --dev patchlevel/event-sourcing-analyser

Register the extension

The package ships an extension.neon that registers the collectors and the output formatters. Include it from your phpstan.neon:

includes:
    - vendor/patchlevel/event-sourcing-analyser/extension.neon

parameters:
    level: max
    paths:
        - src

If you use phpstan/extension-installer the file is included automatically and you can drop the includes block.

Define some events

Events describe what happened in your domain. The analyser finds them by their #[Event] attribute and uses the event name as the label in the diagram.

use Patchlevel\EventSourcing\Attribute\Event;

#[Event('profile.created')]
final class ProfileCreated
{
    public function __construct(
        public readonly string $name,
    ) {
    }
}

#[Event('profile.renamed')]
final class ProfileRenamed
{
    public function __construct(
        public readonly string $name,
    ) {
    }
}

Define the aggregate

The aggregate is the heart of your domain. The #[Aggregate] attribute marks the class, and every $this->recordThat(...) call tells the analyser which event a method records. A method annotated with #[Handle] links the recorded events back to the command that triggered them.

use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
use Patchlevel\EventSourcing\Attribute\Aggregate;
use Patchlevel\EventSourcing\Attribute\Handle;

#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
{
    #[Handle]
    public static function create(CreateProfile $command): self
    {
        $self = new self();
        $self->recordThat(new ProfileCreated($command->name));

        return $self;
    }

    #[Handle(RenameProfile::class)]
    public function rename(string $name): void
    {
        $this->recordThat(new ProfileRenamed($name));
    }
}

The analyser reads the command from the typed parameter of #[Handle] or from its explicit RenameProfile::class argument. Both styles are described on the how it works page.

React with a subscriber

A projector, processor or subscriber reacts to events. Mark the class with #[Projector] (or #[Subscriber] / #[Processor]) and each handler with #[Subscribe]. A processor that dispatches a command via the command bus adds another edge to the diagram.

use Patchlevel\EventSourcing\Attribute\Projector;
use Patchlevel\EventSourcing\Attribute\Subscribe;

#[Projector('profile')]
final class ProfileProjector
{
    #[Subscribe(ProfileCreated::class)]
    public function onCreated(ProfileCreated $event): void
    {
        // write to your read model
    }

    #[Subscribe(ProfileRenamed::class)]
    public function onRenamed(ProfileRenamed $event): void
    {
        // update your read model
    }
}

Render the diagram

Run PHPStan with the Graphviz formatter and pipe the result into the dot binary to produce an image:

vendor/bin/phpstan analyse --error-format=eventSourcingGraphviz ./src | dot -Tpng > profile.png

You now have a profile.png showing the CreateProfile and RenameProfile commands flowing into the Profile aggregate, the events it records and the projector that reacts to them.

Export as JSON

If you would rather feed the model into your own tooling, switch the formatter to JSON:

vendor/bin/phpstan analyse --error-format=eventSourcingJson ./src > profile.json

Result

With a single static analysis run you turned your attributes and method calls into a living diagram of your domain. As your code changes, the diagram changes with it.

Learn more