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.

Bb29f6afb2ea244a12c25e04d46af19c?s=128

Tomasz Kowalczyk

June 13, 2019
Tweet

Transcript

  1. ∙ ∙ in Symfony ∙

  2. architectural patterns

  3. needs they do

  4. it depends™

  5. wait

  6. don't

  7. !==

  8. belongs to

  9. tools responsibility well fault

  10. not the end

  11. library

  12. don't *

  13. Symfony ?

  14. good boy framework!

  15. clear responsibility boundaries

  16. supports

  17. like a framework

  18. forget included in PHP SPL

  19. information flow the architecture

  20. composer create-project symfony/skeleton .

  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
  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
  23. └── src ├── Controller └── Kernel.php

  24. None
  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
  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
  27. not guilty

  28. extremely simplified architectural approaches

  29. ports adapters

  30. CQRS

  31. Domain Driven Design

  32. Event Sourcing

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

  34. src ├── Account │ ├── Application │ ├── Domain │

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

    └── Infrastructure └── Shared ├── Application │ └── CommandInterface.php ├── Domain └── Infrastructure ├── SynchronousCommandBus.php ├── CommandBusInterface.php └── SymfonyKernel.php
  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); } }
  37. src ├── Account │ ├── Application │ ├── Domain │

    │ ├── Account.php │ │ └── AccountRepositoryInterface.php │ └── Infrastructure └── Shared ├── Application │ └── CommandInterface.php ├── Domain └── Infrastructure ├── SynchronousCommandBus.php ├── CommandBusInterface.php └── SymfonyKernel.php
  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; } }
  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
  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; } }
  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); } }
  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; } }
  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
  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); } }
  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']); } }
  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]), ]); } }
  47. Symfony?

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

  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']
  50. services: _defaults: autowire: false autoconfigure: false public: false

  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
  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']
  53. E S

  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
  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; } }
  56. simple, eh?

  57. simple !== easy

  58. Any questions? ask

  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)
  60. 5 30 2 8 knowledge eternal glory Symfopardy! see you

    today at 17:00!
  61. None
  62. None
  63. GENTLEMAN software with manners @tmmx

  64. Thanks!