with prooph
@steph_py
architect @
Quick view of theses concepts
Implements it step by step
Be pragmatic, use only what you need
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, ....
.
├── 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
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
$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
use Ramsey\Uuid\UuidInterface;
class User
{
private $id;
private $firstname;
public function __construct(UuidInterface $id, $firstname, ...)
{
$this->id = $id;
$this->firstname = $firstname;
}
}
Group models around an aggregate root
Create relation only between aggregate roots
EventSourcing needs
It's a subdomain
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
http://getprooph.org/
The CQRS and Event Sourcing Components for PHP
prooph/service-bus
prooph/event-store
prooph/event-sourcing
prooph/proophessor-do
Separate writes and reads in your application.
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
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
<?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;
}
}
<?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
<?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:
- 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
Store events to define the present
user_id | product | quantity |
---|---|---|
xxx | Product 2 | 1 |
It misses a lot of informations
What happens if business ask how many Product 1 has been added to a cart ?
Database storage:
[{
"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 |
Read: Projections
Write: Reconstitute the aggregate root
“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 ...
Create as many projections as you want
Updated asynchronously
On demand: Create, update, reconstruct ...
Client: Yesterday at 08:30pm client X had
a 500 when he tried to add product Y to its cart
Client: Yesterday at 08:30pm client X had
a 500 when he tried to add product Y to its cart
All theses concepts are implemented \o/
EventStore (product) not implemented
PDO event store driver available
A bit of code to use it.
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
Projections
Aggregate root
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());
}
//....
}
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']);
}
// ....
}
<?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.
}
}
<?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);
}
}
<?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
}
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;
}
}
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
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
VIP: @martin fowler, @eric evans, @mathias verraes