Once the analyser has read your domain, it renders the model through a PHPStan error formatter. Two formatters ship
with the package: eventSourcingGraphviz draws a diagram inspired by Event Storming, and
eventSourcingJson exports the same model as data for your own tooling. You pick one with the --error-format option
of phpstan analyse.
The Graphviz formatter prints the model in the DOT language, which you turn
into an image with the dot binary:
vendor/bin/phpstan analyse --error-format=eventSourcingGraphviz ./src | dot -Tpng > graph.pngdot is part of the Graphviz toolkit. Install it with your package manager, for example brew install graphviz on
macOS or apt-get install graphviz on Debian based systems. Swap -Tpng for -Tsvg to get a scalable vector image
instead.
Every bounded context becomes a dotted cluster. Inside it, each aggregate is its own subgraph that groups the commands and events belonging to it, while subscribers and controllers sit next to the aggregates in the same context. The edges follow the flow of your domain:
The node colors match the Event Storming notation, so commands are blue, events orange, aggregates yellow, and so on.
The DOT output is plain text. Pipe it to a file and inspect it, or feed it into any Graphviz compatible viewer rather
than the dot command line tool.
The JSON formatter exposes the analysed model as data, so you can build your own renderer, documentation page or pipeline check on top of it. It prints the whole project as pretty printed JSON:
vendor/bin/phpstan analyse --error-format=eventSourcingJson ./src > event-sourcing.jsonThe top level object mirrors the analysed project. Every collection is keyed by the fully qualified class name of its element:
{
"boundedContexts": {
"Profile": {
"name": "Profile",
"aggregates": ["App\\Profile\\Domain\\Profile"],
"events": ["App\\Profile\\Domain\\ProfileCreated"],
"commands": ["App\\Profile\\Domain\\CreateProfile"],
"subscribers": ["App\\Profile\\Domain\\ProfileProjector"],
"userInterfaces": []
}
},
"aggregates": {
"App\\Profile\\Domain\\Profile": {
"name": "profile",
"class": "App\\Profile\\Domain\\Profile",
"events": ["App\\Profile\\Domain\\ProfileCreated"],
"commands": ["App\\Profile\\Domain\\CreateProfile"]
}
},
"events": {
"App\\Profile\\Domain\\ProfileCreated": {
"name": "profile.created",
"class": "App\\Profile\\Domain\\ProfileCreated"
}
},
"commands": {
"App\\Profile\\Domain\\CreateProfile": {
"name": "CreateProfile",
"class": "App\\Profile\\Domain\\CreateProfile",
"events": ["App\\Profile\\Domain\\ProfileCreated"]
}
},
"subscribers": {
"App\\Profile\\Domain\\ProfileProjector": {
"name": "profile",
"class": "App\\Profile\\Domain\\ProfileProjector",
"type": "projector",
"events": ["App\\Profile\\Domain\\ProfileCreated"],
"commands": []
}
},
"userInterfaces": []
}The boundedContexts entries hold only class names and reference the full elements in the other collections. The
type of a subscriber is one of subscriber, processor or projector, matching the three
subscriber flavours. Names come straight from your attributes: an aggregate or event uses the
name you passed to #[Aggregate] or #[Event], while a command or controller falls back to its short class name.