Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Applying DDD when building Drupal modules using Symfony components (DrupalCamp CS)

Applying DDD when building Drupal modules using Symfony components (DrupalCamp CS)

Talk delivered at DrupalCamp CS 2016 in Bratislava, Slovakia.

2bd48651cd01e0ca2e0a255a63da77aa?s=128

Marek Matulka

May 28, 2016
Tweet

Transcript

  1. Applying DDD when building Drupal modules using Symfony components DrupalCamp

    CS ⚫ Bratislava ⚫ 28 May 2016
  2. Who am I?

  3. Marek Matulka Technical Team Lead at Inviqa @super_marek

  4. None
  5. Background

  6. None
  7. Requirement CMS - manage corporate website - manage other brands’

    websites - build seasonal microsites - integrate with Magento store
  8. None
  9. http://photobucket.com/user/marcrobinsone/media/Drupal%20Comics/marcrobinsonegmailcom__drupalNinja.png.html

  10. Why Drupal? - open source CMS - easy to install

    and manage - most required functionality available out of the box - easy to extend
  11. Why Drupal 7?

  12. None
  13. None
  14. What is Symfony?

  15. is a powerful framework

  16. Doctrine ORM Twig

  17. set of decoupled components

  18. None
  19. Domain Driven Design

  20. In order to create a good software, you have to

    know what the software is all about. – Eric Evans
  21. How do we learn?

  22. Stakeholders

  23. Ask for examples... ...write stories!

  24. Vocabulary

  25. Ubiquitous Language Ubiquitous Language

  26. Ubiquitous Language Ubiquitous Language Domain Expert Analyst Tester Developer Developer

  27. Ubiquitous Language Ubiquitous Language Domain Expert Analyst Tester Developer Developer

    Application Code Acceptance Criteria Specs and documentation
  28. Example?

  29. Example Feature: In order to see correct prices As a

    visitor I need to be able to locate the nearest depot Scenario: Given I am visiting a product page for the first time And I got prompted to locate my nearest depot When I enter my post code Then I should see my local depot information
  30. Example Feature: In order to see correct prices As a

    visitor I need to be able to locate the nearest depot Scenario: Given I am visiting a product page for the first time And I got prompted to locate my nearest depot When I enter my post code Then I should see my local depot information
  31. Domain namespace Acme\Depot; interface DepotLocator { /** * @param string

    $postcode * * @return Depot */ public function locateDepot($postcode); }
  32. Implementation namespace Acme\MagentoAdapter; use Acme\Depot\DepotLocator; class MagentoDepotLocator implements DepotLocator {

    public function locateDepot($postcode) { return $this->entityMapper->mapDepot( $this->clientAdapter->findDepotByPostcode($postcode) );
  33. why interface?

  34. Layered Architecture “High level modules should not depend on lower

    level implementation” – Robert C. Martin User Interface Application Domain Infrastructure
  35. Dependency Inversion Policy Layer <interface> Policy Service Mechanism Layer <interface>

    Mechanism Service Utility Layer
  36. Layered Architecture User Interface Application Domain Infrastructure

  37. Layered Architecture User Interface Application Domain Infrastructure

  38. Clean Architecture Entities Use Cases Controllers Web

  39. Clean Architecture Entities Use Cases Controllers Web Framework Drivers Interface

    Adapters Application Business Rules Enterprise Business Rules
  40. Hexagonal Architecture UI Adapter Log Adapter Data Storage Adapter External

    Data Adapter Application Domain
  41. Hexagonal Architecture Acme\Depot\DepotLocator Acme\MagentoAdapter\MagentoDepotLocator

  42. Hexagonal Architecture Acme\Depot\DepotLocator Acme\MagentoAdapter\MagentoDepotLocator port adapter

  43. Why clean architecture? - your code is clean - decoupled

    from framework - re-usable - easy to test - easier to maintain - easier to change
  44. None
  45. When to use DDD? - complex domain - access to

    domain experts - iterative, long term process - OO design
  46. When not to use DDD? - simple domain - data

    centric domains (CRUD, REST) - prototyping
  47. Symfony with Drupal 7

  48. symfony/dependency-injection symfony/config dependency injection

  49. Dependency Injection - a standardised and centralised way of constructing

    objects in our application
  50. Kernel public function boot() { $this->container = new ContainerBuilder(); $this->container->compile();

    }
  51. Kernel public function boot() { $this->container = new ContainerBuilder(); $loader

    = new YamlFileLoader($this->container, new FileLocator('app/config')); $loader->load('parameters.yml'); $this->container->compile(); }
  52. Kernel public function boot() { $this->container = new ContainerBuilder(); $loader

    = new YamlFileLoader($this->container, new FileLocator('app/config')); $loader->load('parameters.yml'); $loader = new XmlFileLoader($this->container, new FileLocator('app/config')); $loader->load('services.xml'); $this->container->compile(); }
  53. Kernel public function boot() { $this->container = new ContainerBuilder(); $loader

    = new YamlFileLoader($this->container, new FileLocator('app/config')); $loader->load('parameters.yml'); $loader = new XmlFileLoader($this->container, new FileLocator('app/config')); $loader->load('services.xml'); $this->container->registerExtension(new ApplicationExtension()); $this->container->compile(); }
  54. Kernel public function boot() { $this->container = $this->buildContainer(); } /**

    * @return ContainerBuilder */ private function buildContainer() { $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator('app/config')); $loader->load('parameters.yml'); $loader = new XmlFileLoader($container, new FileLocator('app/config')); $loader->load('services.xml'); $container->registerExtension(new ApplicationExtension()); $container->compile(); return $container; }
  55. Kernel public function boot() { if (file_exists($this->cachedContainerFileName)) { require $this->cachedContainerFileName;

    $this->container = new ProjectServiceContainer(); } else { $container = $this->buildContainer(); $dumper = new PhpDumper($container); file_put_contents($this->cachedContainerFileName, $dumper->dump()); $this->container = $container; } }
  56. Kernel public function boot() { if (file_exists($this->cachedContainerFileName)) { require $this->cachedContainerFileName;

    $this->container = new ProjectServiceContainer(); } else { $container = $this->buildContainer(); if ('dev' !== $container->getParameter('environment')) { $dumper = new PhpDumper($container); file_put_contents($this->cachedContainerFileName, $dumper->dump()); } $this->container = $container; } }
  57. parameters.yml application: database: name: acme-dev username: acme-dev password: S0m3R4nD0mP455 host:

    localhost port: ~ soap_client: wsdl: https://api.store.local/v1/?wsdl http_auth_username: acme-dev http_auth_password: S0m3R4nD0mP455 username: acme-dev-user api_key: R4nD0mAp1K3y product_hydrator: product_url_snippet: http://store.local/product/{sku} product_image_url: http://store.local/media/catalog/product/uploads/
  58. services.xml

  59. symfony/console console commands

  60. Import Handler class ImportAndPersistActionHandler { /** * @param ProductImporter $importer

    * @param ProductPersister $persister */ public function __construct(ProductImporter $importer, ProductPersister $persister) { $this->importer = $importer; $this->persister = $persister; } public function importAndPersistProducts() { $productsList = $this->importer->importProducts(); $this->persister->saveAll($productsList); } }
  61. Console command class ImportAndPersistCommand extends Command { public function __construct(ImportAndPersistActionHandler

    $action) { $this->action = $action; parent::__construct(); } protected function configure() { $this->setName('products:import'); } protected function execute(InputInterface $input, OutputInterface $output) { $startTime = microtime(true); $this->action->importAndPersistProducts(); $output->writeln( sprintf("Products import completed in %1.02fs.", microtime(true) - $startTime) ); } }
  62. app/console #!/usr/bin/php <?php require_once (__DIR__ . '/../vendor/autoload.php'); use Acme\Application\Kernel; use

    Symfony\Component\Console\Application; $kernel = new Kernel(); $kernel->boot(); $application = new Application(); $application->addCommands([ $kernel->getContainer()->get('acme.command.import_products'), $kernel->getContainer()->get('acme.command.delete_products'), $kernel->getContainer()->get('acme.command.depot_locate'), ]); $application->run();
  63. symfony/filesystem persisting products

  64. XML Writer class ProductXmlWriter implements ProductPersister { /** * @param

    Filesystem $filesystem * @param string $fileName */ public function __construct(Filesystem $filesystem, string $fileName) { $this->filesystem = $filesystem; $this->fileName = $fileName; } /** * @param ProductList $productList */ public function saveAll(ProductList $productList) { $this->filesystem->dumpFile($this->fileName, $this->renderXml($productList)); } }
  65. Why write XML? - fetching data from Magento and dumping

    it into xml file is quick - xml can be imported by Drupal’s feed importer
  66. Why it didn’t work? - Feed importer was damn slow

    - 10k products imported in 4.5h - Scheduling import with Elysia Cron module was flaky - Scheduled feed importer would add products all over again - resources hungry!
  67. Direct save to the db class DatabaseProductPersister implements ProductPersister {

    private $repository; /** * @param ProductRepository $repository */ public function __construct(ProductRepository $repository) { $this->repository = $repository; } public function saveAll(ProductList $productList) { foreach ($productList as $product) { $this->repository->save($product); } } }
  68. Why direct save to the db? - Drupal promises data

    structure won’t ever change - It’s fast - 40k products in 2 minutes - It works!
  69. composer.json "require": { "php": ">=5.4.0", "symfony/console": "~2.5", "drush/drush": "7.*@dev", "symfony/finder":

    "~2.5", "symfony/dependency-injection": "~2.5", "symfony/config": "~2.5", "symfony/filesystem": "~2.6", "doctrine/dbal": "~2.5", "rhumsaa/uuid": "~2.8" },
  70. container.php <?php require_once DRUPAL_ROOT . '/../vendor/autoload.php'; $kernel = new \Acme\Application\Kernel();

    $kernel->boot(); variable_set('di_container', = $kernel->getContainer());
  71. database.php $databases = [ 'default' => [ 'default' => [

    'database' => $conf['di_container']->getParameter('database.name'), 'username' => $conf['di_container']->getParameter('database.username'), 'password' => $conf['di_container']->getParameter('database.password'), 'host' => $conf['di_container']->getParameter('database.host'), 'port' => $conf['di_container']->getParameter('database.port'), 'driver' => 'mysql', 'prefix' => '', ], ], ];
  72. settings.php <?php ... include_once 'config/container.php'; include 'database.php'; ...

  73. depot_locator.module function get_depot_json($postcode) { global $conf; $depotEntity = variable_get('di_container') ->get('acme.depot_locator')->locateDepot($postcode);

    $depot = $depotEntity->toArray(); set_depot_cookies($depot); return ['depot' => $depot]; }
  74. Project folder structure

  75. None
  76. (to get Drupal 7 working with Symfony components) That’s it!

  77. What we’ve learnt?

  78. as a CMS platform Drupal is great!

  79. Cherry pick what you need! Symfony Components

  80. Try it yourself! Decoupled code

  81. Right tool Choose wisely!

  82. Questions?

  83. Thank you! https://speakerdeck.com/super_marek @super_marek