In the following pages, we will walk through all prooph components step by step and take a look at how they work together. You will learn the basic concepts as well as some advanced techniques to push your PHP applications to the next level and prepare them for the future.
In the prooph ecosystem everything is bound together by messages, so when you want to get started with prooph components you should know the basic building block - prooph messages. Once you know how they work, we give a short overview of CQRS and Event Sourcing and why messages play an important role in this architecture.
To start with the tutorial you need PHP 7.1 and composer installed. If you're on Windows please consider using a Linux VM for the tutorial. The tutorial is tested on Linux only and the commands needed for project set up (not many) are shown in their Linux version only.
Every prooph component deals with messages in one way or another, so we've put them in a common package.
Let's get our hands dirty and install the package. First, create an empty folder called prooph_tutorial
and cd
into it.
$ composer require prooph/common
This command will run composer, generating a fresh composer.json
for us, adding prooph/common
as the first package to our new project.
Create a file called hello_world.php
with the following content and run it with php
:
<?php
//All prooph components enable strict types
declare(strict_types=1);
namespace Prooph\Tutorial;
use Prooph\Common\Messaging\Command;
use Prooph\Common\Messaging\PayloadConstructable;
use Prooph\Common\Messaging\PayloadTrait;
//Require composer's autoloader
require 'vendor/autoload.php';
//Our first message
final class SayHello extends Command implements PayloadConstructable
{
use PayloadTrait;
public function to(): string
{
return $this->payload['to'];
}
}
$sayHello = new SayHello(['to' => 'World']);
echo 'Hello ' . $sayHello->to();
//Hello World
In the script above we created our first message of type Command
.
We also used Prooph\Common\Messaging\PayloadTrait
in conjunction with the Prooph\Common\Messaging\PayloadConstructable
interface
to instantiating our command with a payload
- a simple array - and get access to it using
$this->payload
within the message.
While this is a very easy and fast way to create message classes it is completely optional.
The most important thing to note here is that Prooph\Common\Messaging\Command
implements Prooph\Common\Messaging\Message
<?php
declare(strict_types=1);
namespace Prooph\Common\Messaging;
use DateTimeImmutable;
use Ramsey\Uuid\Uuid;
interface Message extends HasMessageName
{
public const TYPE_COMMAND = 'command';
public const TYPE_EVENT = 'event';
public const TYPE_QUERY = 'query';
/**
* Should be one of Message::TYPE_COMMAND, Message::TYPE_EVENT or Message::TYPE_QUERY
*/
public function messageType(): string;
public function uuid(): Uuid;
public function createdAt(): DateTimeImmutable;
public function payload(): array;
public function metadata(): array;
public function withMetadata(array $metadata): Message;
/**
* Returns new instance of message with $key => $value added to metadata
*
* Given value must have a scalar or array type.
*/
public function withAddedMetadata(string $key, $value): Message;
}
The basic contract defines an immutable message with a unique identifier, a type, a created at timestamp,
a message name (by extending the HasMessageName
interface), payload and metadata.
Payload should only contain scalar types and arrays (no objects) and metadata only scalars to be truly immutable.
Command Query Responsibility Segregation (CQRS for short), first described by Greg Young, is one of the two main patterns we created the prooph components to implement. Our goal is to port his idea to the PHP world and make it easy to use for PHP developers.
To summarize CQRS in one sentence:
A CQRS system is divided into two parts, a write model to handle all state changes and a read model to query that state.
The basic concept is very simple and can be illustrated by a common service class:
<?php
declare(strict_types = 1);
namespace Prooph\Tutorial;
class UserService
{
private $userRepository;
public function __construct(UserRepository $repository)
{
$this->userRepository = $repository;
}
public function createUser(int $id, string $name, string $email): User
{
$user = new User($id);
$user->setName($name);
$user->setEmail($email);
$this->userRepository->save($user);
return $user;
}
public function getUser(int $id): User
{
$user = $this->userRepository->get($id);
if(!$user) {
throw UserNotFoundException::withId($id);
}
return $user;
}
}
The UserService
is responsible for all actions related to a user.
In a CQRS system this looks slightly different:
CreateUserHandler.php
<?php
declare(strict_types = 1);
namespace Prooph\Tutorial;
class CreateUserHandler
{
private $userRepository;
public function __construct(UserRepository $repository)
{
$this->userRepository = $repository;
}
public function createUser(int $id, string $name, string $email): void
{
$user = new User($id);
$user->setName($name);
$user->setEmail($email);
$this->userRepository->save($user);
}
}
UserFinder.php
<?php
declare(strict_types = 1);
namespace Prooph\Tutorial;
class UserFinder
{
private $userRepository;
public function __construct(UserRepository $repository)
{
$this->userRepository = $repository;
}
public function getUser(int $id): User
{
$user = $this->userRepository->get($id);
if(!$user) {
throw UserNotFoundException::withId($id);
}
return $user;
}
}
Instead of one service, we have two separate services one for handling the write action
and one for handling the query. Note that CreateUserHandler::createUser(): void
no longer returns
the new user object. The method signature follows a basic rule of CQRS:
Write operations don't have a return value
While this looks like overkill, it enables you to design write and read sides
independent of each other. Let's look at a read-optimized UserFinder
<?php
declare(strict_types = 1);
namespace Prooph\Tutorial;
class UserFinder
{
private $connection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
public function getUser(int $id): array
{
$userData = $this->connection->findOneBy(['id' => $id]);
if(!$userData) {
throw UserNotFoundException::withId($id);
}
return $userData;
}
}
We no longer use the UserRepository
but instead directly access the database.
So we avoid the object relational mapping and return pure user data from our finder.
We can do this because we know that our read model won't do anything with the data other than
forwarding it to a client that, for example, requires the data in JSON or XML format.
If it is guaranteed that no state changes happen within the read model, we don't need to deal with
objects as we don't need to enforce any rules. Select the data and return it to the client as fast as possible
that is the target of the read model.
The write model, however, has to protect invariants. At the moment our user
object does a bad job of this.
$user = new User($id);
$user->setName($name);
$user->setEmail($email);
These three lines tell us that a User
can exist in our system without a name and an email, only
the id is required. For most systems this is not true. What about this?
$user = User::create($id, $name, $email);
Yeah, looks better now. Let's put it in the handler.
<?php
declare(strict_types = 1);
namespace Prooph\Tutorial;
class CreateUserHandler
{
private $userRepository;
public function __construct(UserRepository $repository)
{
$this->userRepository = $repository;
}
public function createUser(int $id, string $name, string $email): void
{
$user = User::create($id, $name, $email);
$this->userRepository->save($user);
}
}
It is okay but not perfect because we are missing intent. The code does not express why a user is created.
The following code looks better, right?
<?php
declare(strict_types = 1);
namespace Prooph\Tutorial;
class RegisterUserHandler
{
private $userRepository;
public function __construct(UserRepository $repository)
{
$this->userRepository = $repository;
}
public function registerUser(int $id, string $name, string $email): void
{
$user = User::register($id, $name, $email);
$this->userRepository->save($user);
}
}
Finally, we add some prooph flavour and change the method of the handler to handle a prooph message
<?php
declare(strict_types = 1);
namespace Prooph\Tutorial;
class RegisterUserHandler
{
private $userRepository;
public function __construct(UserRepository $repository)
{
$this->userRepository = $repository;
}
public function handle(RegisterUser $command): void
{
$user = User::register($command->userId(), $command->userName(), $command->email());
$this->userRepository->save($user);
}
}
With a few changes we turned our original UserService
into two distinct classes supporting the basic idea of CQRS.
In the last step we enabled the write side to handle a prooph command that expresses its intent of how the system state should change
(a new user should be registered) using the message name, RegisterUser
, and payload of the command.
The RegisterUserHandler
is a so-called glue component. Its task is to take the command and
translate the intent into an action performed by the write model (in our case the user).
Finally, the command handler makes use of the infrastructure (represented by the UserRepository
) to persist
the state change caused by the command.
In an event sourced system all state changes are described by events. The fact that a new user was registered in our system would be one of those events. Another one would be that the user has logged in or changed their email address.
Let's analyze the last example. We start by looking at our database table after the user was registered.
id | name | |
---|---|---|
1 | John Doe | doe@test.com |
Applying CQRS again we end up with a new command ChangeEmail
, an appropriate command handler and a matching action
in the write model owned by the responsible object User::changeEmail
<?php
declare(strict_types = 1);
namespace Prooph\Tutorial;
class ChangeEmailHandler
{
private $userRepository;
public function __construct(UserRepository $repository)
{
$this->userRepository = $repository;
}
public function handle(ChangeEmail $command): void
{
$user = $this->userRepository->get($command->userId());
$user->changeEmail($command->newEmail());
$this->userRepository->save($user);
}
}
Performing a command like this:
$changeEmail = new ChangeEmail([
'userId' => 1,
'newEmail' => 'john.doe@test.com'
]);
$handler->handle($changeEmail);
will result in an updated database row
id | name | |
---|---|---|
1 | John Doe | john.doe@test.com |
What is wrong here? We've changed state but we don't know why and when it happened. Wouldn't it be nice if we could look at the database and see what caused the state change?
What would you say if your database would give you this information instead?
[{
event: UserWasRegistered,
createdAt: 2017-01-13
payload: {id: 1, name: "John Doe", email: "doe@test.com"}
},
{
event: EmailWasChanged,
createdAt: 2017-01-14
payload: {id: 1, newEmail: "john.doe@test.com"}
}]
Welcome to the world of Event Sourcing. This is only the beginning. After completing the walk-through tutorial, you won't want to look back. Event Sourcing makes it so much simpler to create software that reflects intent, and you have all information available to find bugs faster and add new features without pain. Messages are the basic building block but we need to look at a few other things. Fasten your seatbelt and enjoy the journey.
The following pages cover everything you need to know about Event Sourcing, including detailed explanations and working on an example project. If you want to do a quick walk-through instead, or get your hands dirty before the theory, then you can head over to Prooph: CQRS+ES in PHP. How to use. - by Marcin Pilśniak.