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.
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-analyserThe 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.
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,
) {
}
}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.
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
}
}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.pngYou 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.
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.jsonWith 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.