PHP Cody Tutorial
prooph board can connect to a coding bot called Cody. With its help you can generate working code from an event map. This tutorial guides you through the first steps. You’ll learn the basics as well as customizing code generation to suit your needs.
Preparation
We’ve prepared a repository containing some exercises.
Please make sure you have installed Docker and Docker Compose to execute the exercises. Jest is used to validate the results.
- Clone Repo and execute setup
git clone git@github.com:proophboard/php-cody-tutorial.git
cd php-cody-tutorial
./setup.sh
- Create cody tutorial board on prooph board and test connection
You can use prooph board free version for the tutorial (no login required).
prooph board is a modeling tool specifically designed for remote Event Storming. It ships with realtime collaboration features for teams (only available in paid version). The free version is a standalone variant without any backend connection. Your work is stored in local storage and can be exported. It is hosted on Github Pages and has the same code generation capabilities as the SaaS version.
Create a new board called “Cody Tutorial”. You’ll be redirected to the fresh board. Choose “Cody” from top menu to open the Cody Console. Just hit ENTER in the console to connect to the default Cody server that we’ve setup and started in the previous step.
Finally type “/help” in the console to let Cody explain the basic functionality.
Cody Explained
Cody is a http server running in a docker container on your local machine. prooph board can connect to it using /connect [server address]
in the Cody Console. codyconfig.php
is the central place to configure code generation. Generated code needs to be written to a target repository. We can use a docker volume mount to give Cody access to our project repository. In the tutorial our project is represented by exercises
, which contains a PHPUnit test case for each exercise to validate that generated code looks like the expected one.
Exercise I
Your folder structure should look like this:
cody-tutorial
|__ cody-bot
|__ exercises
The Cody Server should be running and listen on http://localhost:3311
:
cd cody-tutorial/cody-bot
docker-compose ps
Name Command State Ports
-----------------------------------------------------------------------------------------
iio_iio-ic-react-http_1 docker-php-entrypoint vend ... Up 0.0.0.0:3311->8080/tcp
Note: Use dev.sh
to start Cody. After that you can use docker-compose commands to stop, start again, inspect logs, etc. A file watcher restarts the server on changes. If something does not work as expected try to restart the server manually and take a look at the logs: docker-compose stop && docker-compose up -d && docker-compose logs -f
Test-Driven Exercises
Navigate to the tutorial project in a second console and run the first exercise using:
cd cody-tutorial/exercises
docker-compose run --rm composer exercise1
Of course the test case is failing. it is looking for a Command called AddBuilding and that’s the first class we want to generate from a prooph board event map. You might have noticed the commented hooks in codyconfig.php
. What we need is a CommandHook
, so let’s create one!
Create Command Hook
# current dir: cody-tutorial/cody-bot
# Create a Hook folder
mkdir src/Hook
# Create CommandHook.php
touch src/Hook/CommandHook.php
Open src/Hook/CommandHook.php
in your favorite editor and copy this content into it:
<?php
declare(strict_types=1);
namespace EventEngine\InspectioCody\Hook;
use EventEngine\InspectioCody\Board\BaseHook;
use EventEngine\InspectioCody\Http\Message\CodyResponse;
use EventEngine\InspectioCody\Http\Message\Response;
use EventEngine\InspectioGraphCody\Node;
final class CommandHook extends BaseHook //BaseHook provides some helper methods like writeFile()
{
/**
* @param Node $command Information about Command sticky received from prooph board event map
* @param object $context Context object populated in codyconfig.php
* @return CodyResponse Response sent back to prooph board, shown in Cody Console
*/
public function __invoke(Node $command, object $context): CodyResponse
{
$commandName = $command->name();
$commandFile = $commandName . '.php';
$commandPath = $context->path . '/Command/' . $commandFile;
$code = <<<CODE
<?php
declare(strict_types=1);
namespace Cody\Tutorial\Command;
class $commandName
{
}
CODE;
$this->writeFile($code, $commandPath);
return Response::fromCody(
"Command \"{$commandName}\" generated",
["Command written to {$commandPath}"]
);
}
}
Register Hook in codyconfig
To activate the hook, we need to register it in codyconfig.php
:
<?php
/**
* @see https://github.com/event-engine/php-inspectio-cody for the canonical source repository
* @copyright https://github.com/event-engine/php-inspectio-cody/blob/master/COPYRIGHT.md
* @license https://github.com/event-engine/php-inspectio-cody/blob/master/LICENSE.md MIT License
*/
declare(strict_types=1);
use EventEngine\InspectioCody\CodyConfig;
// IMPORT COMMAND HOOK
use EventEngine\InspectioCody\Hook\CommandHook;
$context = new class() implements \EventEngine\InspectioCody\CodyContext {
public function isFullSyncRequired(): bool
{
// TODO: Implement isFullSyncRequired() method.
return false;
}
public function clearGraph(): void
{
// TODO: Implement clearGraph() method.
}
}; // replace it with your own context class
$context->path = '/exercises/src';
return new CodyConfig(
$context,
[
// CodyConfig::HOOK_ON_AGGREGATE => new AggregateHook(),
// UNCOMMENT COMMAND HOOK
CodyConfig::HOOK_ON_COMMAND => new CommandHook(),
// CodyConfig::HOOK_ON_EVENT => new EventHook(),
// CodyConfig::HOOK_ON_POLICY => new PolicyHook(),
// CodyConfig::HOOK_ON_DOCUMENT => new DocumentHook(),
]
);
Modeling On Event Map
Cody is now able to turn information from command stickies (on a prooph board event map) into PHP classes. Switch to the Cody Tutorial board in prooph board and add a command sticky (blue one) with label AddBuilding
. Right click on the newly created sticky and choose Trigger Cody from context menu. In the Cody Console you can check the response. Cody should tell you: Command "AddBuilding" generated
.
Awesome, it works! Rerun:
docker-compose run --rm composer exercise1
It should turn green now. The test verifies that a Cody\Tutorial\Command\AddBuilding has been generated by Cody and put into cody-tutorial/exercises/src/Command/AddBuilding.php.
In exercise I we implemented our first Cody Hook and registered it in codyconfig.php
. The hook is called with information received from a prooph board event map
and with a user defined context object, which can be used to pass configuration options (like a source path) to each hook.
We can trigger the hook from prooph board by selecting an appropriate sticky on an event map and Trigger Cody from context menu.
Exercise II
To see what we have to do next, execute:
docker-compose run --rm composer exercise2
We’re asked to add a buildingId
and a name
property (both of type string
) to the AddBuilding
command. Now it would be easy to just open the class and add those two properties. But we should expand our code generation logic instead. This has some significant advantages:
- a prooph board event map acts as documentation
- one can generate contracts like an OpenAPI schema from Card Metadata
- it is easier to discuss and design new features or refactorings when such details are included on the event map
Expand Command Hook Logic
Ok let’s use a simple code generator implementation first to understand the basics. In a later tutorial part we’ll look at some useful libraries that provide abstractions for advanced use cases.
Change the command hook in cody-tutorial/cody-bot/src/Hook/CommandHook.php
like this:
<?php
declare(strict_types=1);
namespace EventEngine\InspectioCody\Hook;
use EventEngine\InspectioCody\Board\BaseHook;
use EventEngine\InspectioCody\Http\Message\CodyResponse;
use EventEngine\InspectioCody\Http\Message\Response;
use EventEngine\InspectioGraphCody\Node;
use function is_array;
use function json_decode;
final class CommandHook extends BaseHook //BaseHook provides some helper methods like writeFile()
{
/**
* @param Node $command Information about Command sticky received from prooph board event map
* @param object $context Context object populated in codyconfig.php
* @return CodyResponse Response sent back to prooph board, shown in Cody Console
*/
public function __invoke(Node $command, object $context): CodyResponse
{
$commandName = $command->name();
$commandFile = $commandName . '.php';
$commandPath = $context->path . '/Command/' . $commandFile;
// Get raw metadata info about command and convert it to associative array
$metadata = json_decode($command->metadata(), true);
$properties = "";
if(is_array($metadata)) {
// @TODO: some structural validation might be a good idea here
foreach ($metadata as $property => $type) {
// Each property-type-pair is appended to properties string
// and resulting string is inserted in the class template below
$properties.= " public $type $$property;\n";
}
}
$code = <<<CODE
<?php
declare(strict_types=1);
namespace Cody\Tutorial\Command;
class $commandName
{
$properties
}
CODE;
$this->writeFile($code, $commandPath);
return Response::fromCody(
"Command \"{$commandName}\" generated",
["Command written to {$commandPath}"]
);
}
}
Set Command Metadata in prooph board
Now we can switch to prooph board and set the following metadata in JSON format to the AddBuilding
command:
{
"buildingId": "string",
"name": "string"
}
Metadata can be set by opening the Metadata Sidebar (choose Metadata from top menu) and selecting the appropriate card or sticky note. Metadata changes are saved automatically.
Once metadata is set we can trigger Cody again …
… and validate the result by executing:
docker-compose run --rm composer exercise2
Cards on a prooph board event map can have additional information set as metadata. By default metadata is stored in JSON format. Metadata itself is schemaless, meaning users are free to define any structure. The structure can be defined and even be type checked using Metadata Templates. In a Cody hook you have access to raw metadata and use it for advanced code generation.
Exercise III
One last basic concept is missing to complete the picture. On a prooph board event map you model message flows, behavior, business processes. Different objects interact with each other by exchanging messages like commands and events. A Cody Hook can make use of this information to assist developers by generating all required glue code if not a fully working implementation (depends on complexity).
We start again by looking at the failing test case for exercise III:
docker-compose run --rm composer exercise3
Ok, this time we’re asked to generate an aggregate class named Building
with a method handling the AddBuilding
command from previous exercises.
Aggregate Hook
In Exercise I you’ve learned how to create a Command Hook and register it in codyconfig.php
.
Do the same for an AggregateHook
and let the hook generate an empty aggregate class. The class should be written to the directory exercises/src/Aggregate
.
If you think the hook is ready, switch to prooph board, add an aggregate card with label Building
on the event map and trigger Cody.
Did it work? You can verify the result by executing:
docker-compose run --rm composer exercise3
Test case is still failing but the error message has changed. Building::addBuilding(AddBuilding $command)
method is still missing.
Event Map Connections
Go back to the event map and draw an arrow from AddBuilding
command to Building
aggregate.
The connection we’ve just drawn can be read in a hook. Here is the Node
interface passed to a hook:
<?php
/**
* @see https://github.com/event-engine/php-inspectio-graph-cody for the canonical source repository
* @copyright https://github.com/event-engine/php-inspectio-graph-cody/blob/master/COPYRIGHT.md
* @license https://github.com/event-engine/php-inspectio-graph-cody/blob/master/LICENSE.md MIT License
*/
declare(strict_types=1);
namespace EventEngine\InspectioGraphCody;
interface Node
{
public function id(): string;
public function name(): string;
public function type(): string;
public function tags(): iterable;
public function isLayer(): bool;
public function isDefaultLayer(): bool;
public function parent(): ?Node;
/**
* @return Node[]
*/
public function children(): iterable;
/**
* @return Node[]
*/
public function sources(): iterable;
/**
* @return Node[]
*/
public function targets(): iterable;
public function metadata(): ?string;
}
We already worked with name()
and metadata()
. From a Node
you can follow connections using sources()
and targets()
. In our case the AddBuilding
command node has the Building
aggregate referenced as a target and from Building
point of view AddBuilding
command is a source.
One important thing to note here is that a hook gets only access to directly connected nodes, even if those nodes have further connections! This avoids circular reference problems and keeps payload exchanged between prooph board and Cody small. However, it’s possible to request a full board sync and build up a node map in Cody. But this is an advanced topic, that should be covered later.
Node
interface also provides information about parent()
and children()
which are useful when grouping cards in a frame
Enough theory! Let’s see it in action. Update your AggregateHook
with this version:
<?php
declare(strict_types=1);
namespace EventEngine\InspectioCody\Hook;
use EventEngine\InspectioCody\Board\BaseHook;
use EventEngine\InspectioCody\Http\Message\CodyResponse;
use EventEngine\InspectioCody\Http\Message\Response;
use EventEngine\InspectioGraph\VertexType;
use EventEngine\InspectioGraphCody\Node;
use LogicException;
use function lcfirst;
final class AggregateHook extends BaseHook
{
/**
* @param Node $aggregate Information about aggregate sticky received from prooph board event map
* @param object $context Context object populated in codyconfig.php
* @return CodyResponse Response sent back to prooph board, shown in Cody Console
*/
public function __invoke(Node $aggregate, object $context): CodyResponse
{
$aggregateName = $aggregate->name();
$aggregateFile = $aggregateName . '.php';
$aggregatePath = $context->path . '/Aggregate/' . $aggregateFile;
$commandHandlingMethod = "";
$includes = "";
$command = null;
foreach ($aggregate->sources() as $source) {
if($source->type() === VertexType::TYPE_COMMAND) {
if($command) {
throw new LogicException(
"Aggregate $aggregateName is connected to more than one command"
);
}
$command = $source;
}
}
if($command) {
$includes.= "use Cody\Tutorial\Command\\{$command->name()};\n";
$methodName = lcfirst($command->name());
$commandHandlingMethod =
"public static function $methodName({$command->name()} \$command): void {}";
}
$code = <<<CODE
<?php
declare(strict_types=1);
namespace Cody\Tutorial\Aggregate;
$includes
class $aggregateName
{
$commandHandlingMethod
}
CODE;
$this->writeFile($code, $aggregatePath);
return Response::fromCody(
"Aggregate \"{$aggregateName}\" generated",
["Aggregate written to {$aggregatePath}"]
);
}
}
The implementation should be self explanatory. We included a logical validation that an aggregate card should only have one command as a source. If more than one is found an exception is thrown. Exceptions are caught by Cody and sent back to prooph board as an error response. This way you can harden your hooks and validate event maps according to your own rules.
Trigger Cody with the Building
aggregate card in prooph board and run exercise III once again:
docker-compose run --rm composer exercise3
This exercise introduced the last basic building block: Connections between cards. Depending on arrow direction on prooph board a connected card appears either in the sources
or targets
collection of the Node
passed to a hook. It’s important to keep in mind that a hook only has access to directly connected cards.
Exercise IV
Exercise IV coming soon!
Meanwhile checkout these libraries for advanced code generation:
- PHP prooph board Graph Cody: High-level abstraction for Cody nodes
- Event Engine Code Generator: High-level abstraction for generating Event Engine code
And have a look at the example boards to learn more about working on prooph board: