$30 off During Our Annual Pro Sale. View Details »

DDD, CQRS, ES, Hexagonal... and Symfony

DDD, CQRS, ES, Hexagonal... and Symfony

We talk a lot about various architectural approaches and how to make our projects better when using them to achieve business goals and technical quality. Unfortunately, we don't talk as much about the environment in which we apply those techniques - the framework - and since they represent the crucial part which determines the amount of work, I think we should. In this talk I'd like to show you how Symfony can be used with virtually any kind of approach - DDD, CQRS, Event Sourcing, Hexagonal Architecture, and many more - and how well it fits in each of those use cases. There are no silver bullets, but some tools do a really good job of letting you take control, and Symfony is definitely one of them.

Tomasz Kowalczyk

June 13, 2019
Tweet

More Decks by Tomasz Kowalczyk

Other Decks in Programming

Transcript

  1. ∙ ∙
    in Symfony

    View Slide

  2. architectural patterns

    View Slide

  3. needs
    they do

    View Slide

  4. it depends™

    View Slide

  5. wait

    View Slide

  6. don't

    View Slide

  7. !==

    View Slide

  8. belongs to

    View Slide

  9. tools
    responsibility well
    fault

    View Slide

  10. not the end

    View Slide

  11. library

    View Slide

  12. don't *

    View Slide

  13. Symfony ?

    View Slide

  14. good boy framework!

    View Slide

  15. clear responsibility
    boundaries

    View Slide

  16. supports

    View Slide

  17. like a framework

    View Slide

  18. forget
    included in PHP SPL

    View Slide

  19. information flow
    the architecture

    View Slide

  20. composer create-project symfony/skeleton .

    View Slide

  21. ├── bin
    │ └── console
    ├── composer.json
    ├── composer.lock
    ├── config
    │ ├── bootstrap.php
    │ ├── bundles.php
    │ ├── packages
    │ │ ├── cache.yaml
    │ │ ├── dev
    │ │ │ └── routing.yaml
    │ │ ├── framework.yaml
    │ │ ├── routing.yaml
    │ │ └── test
    │ │ ├── framework.yaml
    │ │ └── routing.yaml
    │ ├── routes.yaml
    │ └── services.yaml
    ├── public
    │ └── index.php
    ├── src
    │ ├── Controller
    │ └── Kernel.php
    ├── symfony.lock
    ├── var
    └── vendor

    View Slide

  22. ├── bin
    │ └── console
    ├── composer.json
    ├── composer.lock
    ├── config
    │ ├── bootstrap.php
    │ ├── bundles.php
    │ ├── packages
    │ │ ├── cache.yaml
    │ │ ├── dev
    │ │ │ └── routing.yaml
    │ │ ├── framework.yaml
    │ │ ├── routing.yaml
    │ │ └── test
    │ │ ├── framework.yaml
    │ │ └── routing.yaml
    │ ├── routes.yaml
    │ └── services.yaml
    ├── public
    │ └── index.php
    ├── src
    │ ├── Controller
    │ └── Kernel.php
    ├── symfony.lock
    ├── var
    └── vendor

    View Slide

  23. └── src
    ├── Controller
    └── Kernel.php

    View Slide

  24. View Slide

  25. └── src
    ├── Command
    │ └── GenerateCommand.php
    ├── Controller
    │ ├── UserController.php
    │ └── CarController.php
    ├── Entity
    │ ├── User.php
    │ └── Car.php
    ├── Form
    │ ├── UserType.php
    │ └── CarType.php
    ├── Manager
    │ ├── UserManager.php
    │ └── CarManager.php
    ├── Repository
    │ ├── UserRepository.php
    │ └── CarRepository.php
    ├── Security
    │ └── Voter
    │ ├── UserVoter.php
    │ └── CarVoter.php
    ├── Service
    │ ├── UserService.php
    │ └── CarService.php
    └── Kernel.php

    View Slide

  26. └── src
    ├── Command
    │ └── GenerateCommand.php
    ├── Controller
    │ ├── UserController.php
    │ └── CarController.php
    ├── Entity
    │ ├── User.php
    │ └── Car.php
    ├── Form
    │ ├── UserType.php
    │ └── CarType.php
    ├── Manager
    │ ├── UserManager.php
    │ └── CarManager.php
    ├── Repository
    │ ├── UserRepository.php
    │ └── CarRepository.php
    ├── Security
    │ └── Voter
    │ ├── UserVoter.php
    │ └── CarVoter.php
    ├── Service
    │ ├── UserService.php
    │ └── CarService.php
    └── Kernel.php

    View Slide

  27. not guilty

    View Slide

  28. extremely simplified
    architectural approaches

    View Slide

  29. ports
    adapters

    View Slide

  30. CQRS

    View Slide

  31. Domain Driven Design

    View Slide

  32. Event Sourcing

    View Slide

  33. src
    └── Shared
    └── Infrastructure
    └── SymfonyKernel.php

    View Slide

  34. src
    ├── Account
    │ ├── Application
    │ ├── Domain
    │ └── Infrastructure
    └── Shared
    ├── Application
    ├── Domain
    └── Infrastructure
    └── SymfonyKernel.php

    View Slide

  35. src
    ├── Account
    │ ├── Application
    │ ├── Domain
    │ └── Infrastructure
    └── Shared
    ├── Application
    │ └── CommandInterface.php
    ├── Domain
    └── Infrastructure
    ├── SynchronousCommandBus.php
    ├── CommandBusInterface.php
    └── SymfonyKernel.php

    View Slide

  36. interface CommandBusInterface
    {
    public function handle(CommandInterface $command): void;
    }
    final class SynchronousCommandBus implements CommandBusInterface
    {
    private $handlers;
    public function map(string $command, callable $handler): void
    {
    $this->handlers[$command] = $handler;
    }
    public function handle(CommandInterface $command): void
    {
    $fqcn = \get_class($command);
    $handlerNotFound = false === isset($this->handlers[$fqcn]);
    HandlerNotFoundException::throwWhen($handlerNotFound, $fqcn);
    call_user_func($this->handlers[$fqcn], $command);
    }
    }

    View Slide

  37. src
    ├── Account
    │ ├── Application
    │ ├── Domain
    │ │ ├── Account.php
    │ │ └── AccountRepositoryInterface.php
    │ └── Infrastructure
    └── Shared
    ├── Application
    │ └── CommandInterface.php
    ├── Domain
    └── Infrastructure
    ├── SynchronousCommandBus.php
    ├── CommandBusInterface.php
    └── SymfonyKernel.php

    View Slide

  38. interface AccountRepositoryInterface
    {
    public function findById(int $id): Account;
    }
    final class Account
    {
    /** @var int */
    private $id;
    /** @var string */
    private $email;
    public function __construct(int $id, string $email)
    {
    $this->id = $id;
    $this->email = $email;
    }
    }

    View Slide

  39. src
    ├── Account
    │ ├── Application
    │ │ ├── AccountQueryInterface.php
    │ │ ├── SetupAccountCommand.php
    │ │ └── SetupAccountHandler.php
    │ ├── Domain
    │ │ ├── Account.php
    │ │ └── AccountRepositoryInterface.php
    │ └── Infrastructure
    └── Shared
    ├── Application
    │ └── CommandInterface.php
    ├── Domain
    └── Infrastructure
    ├── SynchronousCommandBus.php
    ├── CommandBusInterface.php
    └── SymfonyKernel.php

    View Slide

  40. final class SetupAccountCommand implements CommandInterface
    {
    /** @var int */
    private $id;
    /** @var string */
    private $email;
    public function __construct(int $id, string $email)
    {
    $this->id = $id;
    $this->email = $email;
    }
    public function getId(): int { return $this->id; }
    public function getEmail(): string { return $this->email; }
    }

    View Slide

  41. final class SetupAccountHandler
    {
    /** @var AccountRepositoryInterface */
    private $accounts;
    public function __construct(AccountRepositoryInterface $accounts)
    {
    $this->accounts = $accounts;
    }
    public function __invoke(SetupAccountCommand $cmd)
    {
    $account = new Account($cmd->getId(), $cmd->getEmail());
    $this->accounts->add($account);
    }
    }

    View Slide

  42. interface AccountQueryInterface
    {
    public function findByEmail(EmailValue $email): AccountView;
    }
    final class AccountView
    {
    /** @var int */
    public $id;
    /** @var string */
    public $email;
    public function __construct(int $id, string $email)
    {
    $this->id = $id;
    $this->email = $email;
    }
    }

    View Slide

  43. src
    ├── Account
    │ ├── Application
    │ │ ├── AccountQueryInterface.php
    │ │ ├── SetupAccountCommand.php
    │ │ └── SetupAccountHandler.php
    │ ├── Domain
    │ │ ├── Account.php
    │ │ └── AccountRepositoryInterface.php
    │ └── Infrastructure
    │ ├── AccountController.php
    │ ├── DoctrineDbalAccountQuery.php
    │ └── DoctrineOrmAccountRepository.php
    └── Shared
    ├── Application
    │ └── CommandInterface.php
    ├── Domain
    └── Infrastructure
    ├── SynchronousCommandBus.php
    ├── CommandBusInterface.php
    └── SymfonyKernel.php

    View Slide

  44. final class DoctrineOrmAccountRepository implements AccountRepositoryInterface
    {
    /** @var EntityManagerInterface */
    private $em;
    public function __construct(EntityManagerInterface $em)
    {
    $this->em = $em;
    }
    public function findById(int $id): Account
    {
    return $this->em->find(Account::class, $id);
    }
    }

    View Slide

  45. final class DoctrineDbalAccountQuery implements AccountQueryInterface
    {
    /** @var Connection */
    private $connection;
    public function __construct(Connection $connection)
    {
    $this->connection = $connection;
    }
    public function findByEmail(EmailValue $email): AccountView
    {
    $sql = 'SELECT id, email FROM user WHERE email = :email';
    $data = $this->connection->exec($sql, ['email' => $email]);
    return new AccountView($data['id'], $data['email']);
    }
    }

    View Slide

  46. final class AccountController
    {
    // ...
    public function __construct(CommandBusInterface $bus, RouterInterface $router)
    {
    // ...
    }
    public function setupAction(Request $request): Response
    {
    $id = generateId();
    $payload = $request->request->all();
    $command = new SetupAccountCommand($id, $payload['email']);
    $this->bus->handle($command);
    return new Response(null, Response::HTTP_CREATED, [
    'Location' => $this->router->generate('account.view', [$id]),
    ]);
    }
    }

    View Slide

  47. Symfony?

    View Slide

  48. account.setup:
    methods: [POST]
    path: /accounts
    controller: account.controller::setupAction

    View Slide

  49. parameters:
    services:
    _defaults:
    autowire: true
    autoconfigure: true
    App\:
    resource: '../src/*'
    exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
    App\Controller\:
    resource: '../src/Controller'
    tags: ['controller.service_arguments']

    View Slide

  50. services:
    _defaults:
    autowire: false
    autoconfigure: false
    public: false

    View Slide

  51. account.query:
    class: Thunder\App\Account\Infrastructure\DoctrineDbalAccountQuery
    arguments: ['@doctrine.dbal.connection']
    account.repository:
    class: Thunder\App\Account\Infrastructure\DoctrineOrmAccountRepository
    arguments: ['@doctrine.orm.entity_manager']
    account.controller:
    class: Thunder\App\Account\Infrastructure\AccountController
    arguments: ['@command_bus', '@routing']
    public: true

    View Slide

  52. command_bus:
    class: Thunder\App\Shared\Infrastructure\SynchronousCommandBus
    arguments: []
    calls:
    - ['map', ['Thunder\App\Account\Application\SetupAccountCommand','@account.setup.handler']]
    account.setup.handler:
    class: Thunder\App\Account\Application\AddAccountHandler
    arguments: ['@account.repository']

    View Slide

  53. E S

    View Slide

  54. src
    ├── Account
    │ ├── Application
    │ │ ├── AccountQueryInterface.php
    │ │ ├── SetupAccountCommand.php
    │ │ └── SetupAccountHandler.php
    │ ├── Domain
    │ │ ├── Account.php
    │ │ ├── AccountWasSetupEvent.php
    │ │ └── AccountRepositoryInterface.php
    │ └── Infrastructure
    │ ├── AccountController.php
    │ ├── DoctrineDbalAccountQuery.php
    │ └── DoctrineOrmAccountRepository.php
    └── Shared
    ├── Application
    │ └── CommandInterface.php
    ├── Domain
    └── Infrastructure
    ├── SynchronousCommandBus.php
    ├── CommandBusInterface.php
    └── SymfonyKernel.php

    View Slide

  55. final class AccountWasSetupEvent
    {
    /** @var int */
    private $id;
    /** @var string */
    private $email;
    public function __construct(int $id, string $email)
    {
    $this->id = $id;
    $this->email = $email;
    }
    public function getId(): int { return $this->id; }
    public function getEmail(): string { return $this->email; }
    }

    View Slide

  56. simple, eh?

    View Slide

  57. simple !== easy

    View Slide

  58. Any questions?
    ask

    View Slide

  59. Resources
    https://murze.be/hexagonal-architecture
    https://fideloper.com/hexagonal-architecture
    https://stackoverflow.com/q/32216408/443341
    https://www.slideshare.net/profpv/hexagonal-architecture-in-php
    https://matthiasnoback.nl/2017/08/layers-ports-and-adapters-part-2-layers
    https://enterprisecraftsmanship.com/2019/01/31/cqrs-commands-part-domain-model
    https://blog.octo.com/en/hexagonal-architecture-three-principles-and-an-implementation-example
    https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/
    https://apiumhub.com/tech-blog-barcelona/applying-hexagonal-architecture-symfony-project
    Images
    https://unsplash.com/photos/cjNaoIqbWCI (dog)
    https://unsplash.com/photos/6U-sSfBV-gM (city tower)
    https://unsplash.com/photos/aSCx7M1E4Vo (foggy forest)
    https://unsplash.com/photos/h0Vxgz5tyXA (wooden floor)

    View Slide

  60. 5 30
    2 8
    knowledge eternal glory
    Symfopardy!
    see you today at 17:00!

    View Slide

  61. View Slide

  62. View Slide

  63. GENTLEMAN
    software with manners
    @tmmx

    View Slide

  64. Thanks!

    View Slide