DDD/CQRS/ES
with prooph
Who am I
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630472/avatar.jpeg)
@steph_py
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630477/wake-on-web.png)
architect @
Today
Quick view of theses concepts
Implements it step by step
Be pragmatic, use only what you need
Domain
Driven
Design
Ubiquitous Language
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4640402/ubiquitous-language.jpg)
File structure
Application
Domain
Infra
UI
Controllers, Console commands, Form types, DTOs
Symfony Kernel, CQRS Commands/Handlers, ...
Your domain logic ... the most important part.
Doctrine, Redis, Guzzle, ....
File structure
.
├── App
│ ├── Command
│ │ └── CreateUserCommand.php
│ ├── CommandHandler
│ │ └── CreateUser.php
│ └── SymfonyBundle
│ └── MyBundle.php
├── Domain
│ ├── User.php
│ └── UserRepositoryInterface.php
├── Infra
│ └── DoctrineORM
│ ├── Resources
│ │ └── User.orm.xml
│ └── UserRepository.php
└── UI
├── Controller
│ └── UserController.php
└── Rest
└── UserNormalizer.php
Model driven design
The model should express your domain
class User
{
private $firstname;
private $lastname;
private $enabled;
public function setFirstname(string $firstname): User
{
$this->firstname = $firstname;
return $this;
}
public function getFirstname(): string
{
return $this->firstname;
}
public function setLastname(string $lastname): User
{
$this->lastname = $lastname;
return $this;
}
public function getLastname(): string
{
return $this->lastname;
}
public function setEnabled(bool $enabled): User
{
$this->enabled = $enabled;
return $this;
}
public function getEnabled(): bool
{
return $this->enabled;
}
}
<?php
// user creation
$user = new User();
$user->setFirstname('Chuck');
$user->setLastname('Norris');
$user->setEnabled(false);
// user rename
$user->setFirstname('Bruce');
$user->setLastname('Lee');
// user enable
$user->setEnabled(true);
User state can be inconsistent
Do not express the intention
SGBD automatism ?
class User
{
private $firstname;
private $lastname;
private $enabled;
public function __construct(string $firstname, string $lastname)
{
$this->firstname = $firstname;
$this->lastname = $lastname;
$this->enabled = false;
}
public function rename(string $firstname, string $lastname): void
{
$this->firstname = $firstname;
$this->lastname = $lastname;
}
public function enable(): void
{
$this->enabled = true;
}
public function getFirstname(): string
{
return $this->firstname;
}
public function getLastname(): string
{
return $this->lastname;
}
public function isEnabled(): bool
{
return $this->enabled;
}
}
<?php
// user creation
$user = new User('Chuck', 'Norris');
// user rename
$user->rename('Bruce', 'Lee');
// user enable
$user->enable();
User state is always consistent
Express the domain
At this moment, we can't disable an user ? It's ok ...
Can't have auto-generated code
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630676/memefu.jpeg)
Please ... don't be lazy
Value object
- Model without an identity.
- Immutable state
- Edition should create a new instance
- Some examples: (dates, prices, addresses, ...)
$start = new \DateTime('08:00:00');
$end = $start->modify('12:00:00');
var_dump($start); // ... 12:00:00 <--- OOPS
$start = new \DateTimeImmutable('08:00:00');
$end = $start->modify('12:00:00');
var_dump($start); // ... 08:00:00
Entity
- Model with an identity.
- Avoid auto-generated Ids
use Ramsey\Uuid\UuidInterface;
class User
{
private $id;
private $firstname;
public function __construct(UuidInterface $id, $firstname, ...)
{
$this->id = $id;
$this->firstname = $firstname;
}
}
Aggregate
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4640453/Blank_Flowchart_-_New_Page__1_.png)
Group models around an aggregate root
Create relation only between aggregate roots
EventSourcing needs
Bounded contexts
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4640421/Blank_Flowchart_-_New_Page.png)
It's a subdomain
Bounded contexts
Models can be different between bounded contexts
Microservice approach
Isolated: A model cannot communicate with a model outside of its BC.
Communication between BC via Events/Commands/Queries (SAGA)
Models are lighter
Responsability separation
Easier to find a feature
Prooph
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
http://getprooph.org/
The CQRS and Event Sourcing Components for PHP
- 1st commit in 2014
- +70 repositories in github
- Active collaborators
- Can be used with Symfony/Laravel
- Great documentation
Trending repositories
prooph/service-bus
prooph/event-store
prooph/event-sourcing
prooph/proophessor-do
Command
Query
Responsibility Segregation
Separate writes and reads in your application.
Title Text
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4652447/Blank_Flowchart_-_New_Page__3_.png)
Buses
In computer architecture, a bus is a communication system that transfers data between components inside a computer, or between computers.
Command bus: 1 handler which returns nothing
Query bus: 1 finder which returns a promise
Event bus: 0,n handler which returns nothing
Can be executed through
- direct
- amqp
- http
- ...
prooph_service_bus:
command_buses:
synchronous_command_bus: ~
asynchronous_command_bus:
router:
# Should implement Prooph\ServiceBus\Async\MessageProducer
async_switch: 'your_producer_service'
<?php
$bus = $container->get('prooph_service_bus.asynchronous_command_bus');
$bus->dispatch(
Acme\Command\RegisterUser::withData('uuid ...', 'chuck', 'chuck@norris.tld')
);
Async producer through bernard/bernard in
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
Command
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
<?php
namespace Prooph\ProophessorDo\Model\User\Command;
/** uses **/
final class RegisterUser extends Command implements PayloadConstructable
{
use PayloadTrait;
public static function withData(UuidInterface $userId, string $name, string $email): RegisterUser
{
return new self([
'user_id' => (string) $userId,
'name' => $name,
'email' => $email,
]);
}
public function userId(): UuidInterface
{
return Uuid::fromString($this->payload['user_id']);
}
protected function setPayload(array $payload): void
{
Assertion::keyExists($payload, 'user_id');
Assertion::uuid($payload['user_id']);
Assertion::keyExists($payload, 'name');
Assertion::string($payload['name']);
Assertion::keyExists($payload, 'email');
$validator = new EmailAddressValidator();
Assertion::true($validator->isValid($payload['email']));
$this->payload = $payload;
}
}
Command Handler
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
<?php
declare(strict_types=1);
namespace Prooph\ProophessorDo\Model\User\Handler;
/** uses **/
class RegisterUserHandler
{
public function __invoke(RegisterUser $command): void
{
// Do your logic here.
}
}
Prooph\ProophessorDo\Model\User\Handler\RegisterUserHandler:
arguments:
public: true
tags:
- { name: 'prooph_service_bus.synchronous_command_bus.route_target', message_detection: true }
Define the command handler as service.
Route it on your bus
Query finder
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
<?php
namespace Prooph\ProophessorDo\Model\User\Handler;
/** uses **/
class GetAllUsersHandler
{
public function __invoke(GetAllUsers $query, Deferred $deferred = null)
{
//$users = ...;
if (null === $deferred) {
return $users;
}
$deferred->resolve($users);
}
}
<?php
$users = [];
$this->queryBus
->dispatch(new GetAllUsers())
->then(
function (\stdClass $result = null) use (&$users) {
$users = $result;
}
);
Usage:
Why ?
- Separate Write & Read (Apps do 90% of read)
- Async/Sync writes, switch is easy.
- Reuse your commands/queries
- Writes depends only on a bus
- Cache management
Event
Sourcing
Store events to define the present
- added 2 “Product 1“ to its cart
- added 1 “Product 2“ to its cart
- removed “Product 1“ from its cart
user_id | product | quantity |
---|---|---|
xxx | Product 2 | 1 |
Legacy approach
It misses a lot of informations
What happens if business ask how many Product 1 has been added to a cart ?
Database storage:
ES approach
[{
"uuid": "956b8f20-818f-4cd7-af42-6f97e20a7c77",
"event": "ProductAddedToCart",
"created_at": "2018-02-26T13:37:00Z",
"payload": {"product": "product 1", "quantity": "2"}}
},
{
"uuid": "956b8f20-818f-4cd7-af42-6f97e20a7c77",
"event": "ProductAddedToCart",
"created_at": "2018-02-26T15:38:00Z",
"payload": {"product": "product 2", "quantity": "1"}}
},
{
"uuid": "956b8f20-818f-4cd7-af42-6f97e20a7c77",
"event": "ProductRemovedFromCart",
"created_at": "2018-02-26T15:39:00Z",
"payload": {"product": "product 1"}}
}]
Event store
+
Projection(s)
user_id | product | quantity |
---|---|---|
xxx | Product 2 | 1 |
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4647776/Blank_Flowchart_-_New_Page__1_.png)
Read: Projections
Write: Reconstitute the aggregate root
![](https://media.giphy.com/media/3o7abrH8o4HMgEAV9e/giphy.gif)
“What about performances ?
If I have 5000 events by aggregate root“
You can drop then recreate your snapshots ...
Create a snapshot of your aggregate root each X events ...
Snapshots
Create as many projections as you want
Projections
Updated asynchronously
On demand: Create, update, reconstruct ...
Without ES
Client: Yesterday at 08:30pm client X had
a 500 when he tried to add product Y to its cart
![](https://media.giphy.com/media/xTiTnJ3BooiDs8dL7W/giphy.gif)
- logs
- database
- ...
- var_dump($x); exit;
With ES
Client: Yesterday at 08:30pm client X had
a 500 when he tried to add product Y to its cart
![](https://media.giphy.com/media/XreQmk7ETCak0/giphy.gif)
- logs
- replay all events until failing event
- reproduce error ...
- Add a test with this scenario ...
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
In Prooph
All theses concepts are implemented \o/
EventStore (product) not implemented
PDO event store driver available
A bit of code to use it.
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
In Prooph
1 event stream per aggregate
1 event stream per aggregate type
1 event stream for all
With PDO
1 event stream = 1 table
Strategies:
prooph_event_store:
stores:
cart_store:
event_store: Prooph\EventStore\Pdo\MySqlEventStore
repositories:
cart_list:
repository_class: Acme\Cart\Infra\Repository\EventStoreCartList
aggregate_type: Acme\Cart\Domain\Cart
aggregate_translator: prooph_event_sourcing.aggregate_translator
projection_managers:
cart_projection_manager:
event_store: Prooph\EventStore\Pdo\MySqlEventStore
connection: 'doctrine.pdo.connection'
projections:
cart_projection:
read_model: Acme\Cart\Infra\Projection\CartReadModel
projection: Acme\Cart\Infra\Projection\CartProjection
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
Projections
Aggregate root
Symfony
final class Cart extends AggregateRoot implements Entity
{
// ....
public function addProduct(
UuidInterface $productId,
int $quantity
): User {
$self->recordThat(ProductWasAddedToCart::withData(
$this->cartId,
$productId,
$quantity
));
}
protected function whenProductWasAddedToCart(ProductWasAddedToCart $event): void
{
if ($this->products->has($event->getProductId()) {
$this->products->get($event->getProductId())->incrementQuantity($event->getQuantity());
return;
}
$this->products->add($event->getProductId(), $event->getQuantity());
}
//....
}
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
Aggregate root
final class ProductWasAddedToCart extends AggregateChanged
{
public static function withData(UuidInterface $cartId, UuidInterface $productId, int $quantity)
{
return self::occur($cartId->toString(), [
'product_id' => $productId->toString(),
'quantity' => $quantity
]);
return $event;
}
public function getCartId(): UuidInterface
{
return Uuid::fromString($this->aggregateId());
}
public function getProductId(): UuidInterface
{
return Uuid::fromString($this->payload['user_id']);
}
// ....
}
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
Event
<?php
namespace Acme\Cart\Infra\Repository;
final class EventStoreCartList extends AggregateRepository implements CartList
{
public function save(Cart $cart): void
{
$this->saveAggregateRoot($cart);
// store all new events raised into event store.
}
public function get(UuidInterface $uuid): ?Cart
{
return $this->getAggregateRoot($uuid->toString());
// fetch all events from event store then handle them.
}
}
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
Cart list (repository)
<?php
namespace Acme\Cart\Domain\Repository;
interface CartList
{
public function save(Cart $cart): void;
public function get(UuidInterface $uuid): ?Cart;
}
class AddProductToCartHandler
{
private $cartList;
public function __construct(CartList $cartList)
{
$this->cartList = $cartList;
}
public function __invoke(AddProductToCart $command): void
{
$cart = $this->cartList->get($command->getCartId());
if (!$cart) {
throw CartNotFound::withCartId($command->getCartId());
}
$cart->addProduct($command->getProductId(), $command->getQuantity());
$this->cartList->save($cart);
}
}
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
Command handler
<?php
final class CartReadModel extends AbstractReadModel
{
public function init(): void
{
$sql = <<<EOF
CREATE TABLE `cart_items` (
`cart_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL,
`product_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL,
// .........
EOF;
$statement = $this->connection->prepare($sql);
$statement->execute();
}
public function isInitialized(): bool {} // is table exists
public function reset(): void {} // truncate
public function delete(): void {} // drop table
protected function insert(array $data): void
{
$this->connection->insert('cart_items', $data);
}
protected function delete(array $data, array $identifier): void {} // do delete
}
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
Projections: read model
final class CartProjection implements ReadModelProjection
{
public function project(ReadModelProjector $projector): ReadModelProjector
{
$projector->fromStream('event_stream')
->when([
ProductWasAddedToCart::class => function ($state, ProductWasAddedToCart $event) {
$readModel = $this->readModel();
$readModel->stack('insert', [
'cart_id' => $event->getCartId()->toString(),
'product_id' => $event->getProductId()->toString(),
'quantity' => $event->getQuantity()
]);
},
ProductWasRemovedFromCart::class => function ($state, ProductWasRemovedFromCart $event) {
$readModel = $this->readModel();
$readModel->stack(
'delete',
[
'cart_id' => $event->getCartId()->toString(),
'product_id' => $event->getProductId()->toString(),
],
);
},
]);
return $projector;
}
}
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
Projections
![](https://s3.amazonaws.com/media-p.slid.es/uploads/833971/images/4630481/prooph.png)
Projections
Projection listening ...
php bin/console event-store:projection:run cart_projection
Projection states in your data storage
id | name | position | state | status |
---|---|---|---|---|
1 | cart_projection | {"event_stream“: 14} | [] | running |
Catch all business data
Cache with projections
Tests: definition, fast execution
Evolve yours projections with past events
Async projection update
You are the time master !
Commands+Events+Projections can be heavy
![](https://media.giphy.com/media/13WVgB4UR7hK/giphy.gif)
Today with prooph
Miss EventStore implementation
Move fast, current version: 6.2.2
No stable release (1.x) for Symfony at this moment
Easy to use
Nice documentation/examples
Unit tested
Active & serious contributors
Questions ?
Documentation
![](https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL._SX375_BO1,204,203,200_.jpg)
VIP: @martin fowler, @eric evans, @mathias verraes
DDD/CQRS/ES
By Stéphane PY
DDD/CQRS/ES
- 2,186