Hey everyone. I'm glad you made it here. I'll try to explain how to "add functionality to the model" by using an example: adding a command to rename a Herd.
A Herd is formed as soon as a Shepherd registers. Their Herd is created with the 'display name' they enter on registration, but after that there is no way for a Shepherd to change their Herds name. Oh nose! Let's fix that.
*What do I mean when I talk about "the model"? The model is the representation of 'reality' in our software.
So where does this belong? Luckily, this is a very small change. A Herd name clearly belongs in the Herding model. Take a look at the Herding model's description in DomainModel.md. If the new functionality warrants a change in the model's description, change it to reflect the new-and-improved model, and maybe log any decisions for future reference!
The first step to to adding our new functionality is thinking about all the moving parts:
Command -> CommandHandler -> Aggregate -> Event -> Projection
The model is described with a tool called PHPspec. It's a tool meant for BDD, or Behavior Driven Development. That means, we start by 'describing' the new parts we need. These 'specifications', or 'specs', are also runnable - and serve as tests that we can use to keep describing new functionality later on! Go ahead and run the current specs to see if they pass.
# Commands are run from inside the vagrant box
> vendor/bin/phpspec run
If you want, take a look at the specifications. They live in the /spec/Elewant/Herding folder. If everything is cool, we start describing our first new part:
> vendor/bin/phpspec describe Elewant/Herding/Model/Commands/RenameHerd
This creates a new specification file: /spec/Elewant/Herding/Model/Commands/RenameHerdSpec.php
Then you run the test suite again:
> vendor/bin/phpspec run
Then PHPspec will ask to create the new class: /src/Elewant/Herding/Model/Commands/RenameHerd.php
This Getting started guide is not aimed at explaining PHPspec, but it's good to know that we describe the behaviour of the model. So we think about what is needed in our shiny new command, and start writing the specifications for it. Take a sneak peek at the end result RenameHerdSpec.php and the corresponding class RenameHerd.php.
So we are trying to change a single property of a single class. But by our latest calculations, we will need to add 3 specifications and 3 classes (Command, CommandHandler and Event), and changing a few more (Aggregate and Projection). What's up with that?
Well, this is an event-sourced system. That means we need the event in order to store it. And the event comes from the Aggregate that was changed. But besides that, we also have a separation between code that changes the Aggregate, and code that views it. That's called CQRS, or Command Query Responsibility Segregation.
It goes hand-in-hand with Event sourcing, but that does mean we get more parts to deal with. Luckily, they are really small parts, very easily reasoned about.
We also use prooph/event-sourcing. This is a library used to build event-sourced systems, and some of the steps we are about to take are made easier by using the exisint building blocks provided by Prooph.
The commands extend Prooph\Common\Messaging\Command
and use a Prooph\Common\Messaging\PayloadTrait
.
You give a new command a static constructor with type-hinted parameters in order to create it. That method just calls it's own constructor with an array, where every key is a piece of the data you want to send with the command. Since we want to send commands cross boundaries, the values must be scalar only so they are easily serialized.
In order to rename a herd, we need to know two things: which herd is it, and what is the new name?
public static function forShepherd(string $herdId, string $newHerdName): self
{
return new self(['herdId' => $herdId, 'newHerdName' => $newHerdName]);
}
Also, each command should get methods that return the data. If needed, these methods return the data in a form that is more suitable in the domain. For example, the herd-id is returned as a HerdId value object:
public function herdId(): HerdId
{
return HerdId::fromString($this->payload['herdId']);
}
public function newHerdName(): string
{
return $this->payload['newHerdName'];
}
The commit for these changes: https://github.com/f500/elewant/commit/725660104757273277d7e34f4e10a094b22c9d14
The commandHandlers are nothing special. They take the command, load the aggregate, then ask the aggregate to do the requested change. Make sure you inject any dependencies, and give the the commandHandler an __invoke method that receives the command.
public function __invoke(RenameHerd $command)
{
// load the herd
$herd = $this->herdCollection->get($command->herdId());
// perform the requested command
$herd->rename($command->newHerdName());
// save the herd
$this->herdCollection->save($herd);
}
Now looking at the commandHandler, that will fail because the aggregate (Herd) doesn't actually know how
to ->rename()
yet. But the aggregate can't do that without throwing the event, so it makes sense to continue
on the event first. Give the herd an empty ->rename()
method if you like to see a green testresult.
The commit for these changes: https://github.com/f500/elewant/commit/cba49d3e7fd2620a3deae29c06f4abccdc8aef88
The events are once again made easier by the Prooph tooling. They extend Prooph\EventSourcing\AggregateChanged
.
A similar concept applies as with the command, except the static constructor is always called tookPlace
:
public static function tookPlace(HerdId $herdId, string $newHerdName): self
{
return self::occur($herdId->toString(), ['newHerdName' => $newHerdName]);
}
Another difference is that all events take place for a certain Aggregate. So events are always created with an aggregate Id as a first parameter, followed by an array of parameters with data. The same rules apply, these events need to be easily serialized so only scalar values.
As with commands, the events need methods that can return the data, in the proper object form if that applies:
public function herdId(): HerdId
{
return HerdId::fromString($this->aggregateId());
}
public function newHerdName(): string
{
return $this->payload['newHerdName'];
}
If you remember correctly, we always start with the specification. Take a look at the final result for the spec HerdWasRenamedSpec.php as well as the event class HerdWasRenamed.php.
The commit for these changes: https://github.com/f500/elewant/commit/74a45da08a663c983efc25f302d38dbfbd77a5cb
Finally! We get to actually change the name of the herd. Oh, no, wait. We need to write the specification for that behavior first. We update the HerdSpec.php to let it know what we want it to do:
# /spec/Elewant/Herding/Model/HerdSpec.php
function it_can_be_renamed()
{
$this->name()->shouldEqual($this->herdName);
$this->rename('new name');
$this->name()->shouldEqual('new name');
}
Now let's open the Herd aggregate, and add that ->rename()
method:
# /src/Elewant/Herding/Model/Herd.php
public function rename(string $newName): void
{
$this->guardIsNotAbandoned();
$this->recordThat(
HerdWasRenamed::tookPlace(
$this->herdId,
$newName
)
);
}
As you can see, when we add the possibility of something changing our aggregate, we always want to guard against impossible things happening. To make sure we don't change things that should not change. We can't go around renaming Herds that where previously abandoned.
Furthermore, we haven't actually changed the name! All we did (for now) was say that we want to record that the Herd rename has taken place. This is the Event class we just wrote. In an event-sourced system, there is a separation between recording events (done only once), and applying events (done every time we need the current state of the aggregate by replaying all the events that ever happened to it).
*a good resource to get more familiar with those concepts is http://buttercup-php.github.io/protects/
So besides writing the recording part, we also need to write the applying part, which is in the aptly-named
apply()
function. Because we want to be real clear on what is needed to apply a certain event, we use the
generic method to find out which AggregateChanged
event we received and then call a specific method.
# /src/Elewant/Herding/Model/Herd.php
switch (get_class($event)) {
...
case HerdWasRenamed::class:
/** @var HerdWasRenamed $event */
$this->applyHerdWasRenamed($event->herdId(), $event->newHerdName());
break;
...
}
And write a specific method to perform the change:
# /src/Elewant/Herding/Model/Herd.php
private function applyHerdWasRenamed(string $newHerdName)
{
$this->name = $newHerdName;
}
And that's where the name is finally changed! So now we're done, right?
The commit for these changes: https://github.com/f500/elewant/commit/acd2f99930690dc31c88fc6bb6ea36b9092d9e2a
The model is now updated to handle a RenameHerd
command. This means that we can change the data (the command
side of CQRS). But we also have a read side. In an event-sourced system, that is usually done by listening to
all the events that can happen, and updating a representation of those events optimized for reading.
This read model can be really simple, in our case the current state of all Herds is updated by the projector into two mysql tables. So anytime we want to only look at Herds, we are looking at the data in those tables.
The projector in question is HerdProjector.php, and it needs a way of handling the rename command:
# /src/Elewant/Herding/Projections/HerdProjector.php
public function onHerdWasRenamed(HerdWasRenamed $event)
{
$this->connection->update(
self::TABLE_HERD,
['name' => $event->newHerdName(),],
['herd_id' => $event->herdId()->toString(),]
);
}
This is just an SQL statement saying UPDATE 'herd table' SET 'name' = 'new value' WHERE herd_id = 'id';
.
Surely, now we're done, right? Right?? (you can probably tell where this is going)
The commit for these changes: https://github.com/f500/elewant/commit/0427f92d731dba9cc4ef4684da00a305dad9bf2c
Aha! You thought you caught me there, didn't you? We wrote specifications for all the other classes, but there is no specification for the projection! Well, that's true. We made a decision that in order to properly test if the system can handle a certain command, we would want to actually fire that command, then check the values in the test database to make sure everything is in order. It's more of an end-to-end test.
It also proves if we properly wired all the parts together in the configuration. Up to this part, we've only concerned ourselves with the model, and not so much with the outside world. But this model is in fact part of an application that uses it. And so we need to define all the bits and pieces in configuration:
# /src/Elewant/AppBundle/Resources/config/service_bus.yml
prooph_service_bus.command_buses.herding_command_bus.router.routes:
...
'Elewant\Herding\Model\Commands\RenameHerd': 'elewant.rename_herd_handler'
prooph_service_bus.event_buses.herd_event_bus.router.routes:
...
'Elewant\Herding\Model\Events\HerdWasRenamed':
- 'elewant.herd_projection.herd_projector'
prooph_service_bus.event_buses.herd_replay_bus.router.routes:
...
'Elewant\Herding\Model\Events\HerdWasRenamed':
- 'elewant.herd_projection.herd_projector'
# /src/Elewant/AppBundle/Resources/config/services.yml
elewant.rename_herd_handler:
class: Elewant\Herding\Model\Handlers\RenameHerdHandler
arguments:
- "@herd_collection"
The commit for these changes: https://github.com/f500/elewant/commit/bfd3433c5711d084df950de928669cdabd956b56
You can find these tests in the /tests/AppBundle/Controller folder. For each of the possible commands, there
is a separate test case. So we need to create a new one there called ApiCommandRenameherdTest.php
. For your
convenience, the tests extend an ApiCommandBase
class that know how to call all the commands on the testApi.
So we need to add a method there that does that for us (you could put this code inside yur actual test, but this
makes it easy to re-use the rename
command in different tests if needed).
# tests/AppBundle/Controller/ApiCommandBase.php
protected function renameHerd(HerdId $herdId, string $newHerdName)
{
$payload = [
'herdId' => $herdId->toString(),
'newHerdName' => $newHerdName,
];
return $this->request('POST', '/testapi/commands/rename-herd', $payload);
}
And in order for this test to run, we need to configure a route for the testApi controller:
# /src/Elewant/AppBundle/Resources/config/routing.yml
'command::rename-herd':
path: '/testapi/commands/rename-herd'
defaults: { _controller: elewant.api_command_controller:postAction, prooph_command_name: 'Elewant\Herding\Model\Commands\RenameHerd' }
The commit for these changes: https://github.com/f500/elewant/commit/3ea007674af3f875b7c895f46b74ef19d9457a0e
These routes only exist in a development environment, and are used to run the end-to-end tests. They are also run Travis every time you finish your work and make a Pull Request.
Speaking of which... shouldn't we be about done by now?!
Well, let's run all the tests, to make sure everything works as intended:
bin/run_tests
Hopefully, everything is green and you are good to go. Excellent job in adding functionality to the Model!