DDD/CQRS/ES

 

with prooph

Who am I

@steph_py

architect @

Today

Quick view of theses concepts

Implements it step by step

Be pragmatic, use only what you need

Domain

Driven

Design

Ubiquitous Language

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

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

Group models around an aggregate root

Create relation only between aggregate roots

EventSourcing needs

Bounded contexts

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

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

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

wakeonweb/service-bus-bundle

Command

<?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

<?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

<?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

  1. added 2 “Product 1“ to its cart
  2. added 1 “Product 2“ to its cart
  3. 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

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 ...

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

  • 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

  • logs
  • replay all events until failing event
  • reproduce error ...
  • Add a test with this scenario ...

In Prooph

All theses concepts are implemented \o/

EventStore (product) not implemented

PDO event store driver available

A bit of code to use it.

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

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());
    }

    //....
}

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']);
    }

    // ....
}

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.
    }
}

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);
    }
}

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
}

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;
    }
}

Projections

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

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

VIP: @martin fowler,  @eric evans, @mathias verraes

DDD/CQRS/ES

By Stéphane PY

DDD/CQRS/ES

  • 494
Loading comments...