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

Applying DDD when building Drupal modules using...

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

Talk delivered at DrupalCamp CS 2016 in Bratislava, Slovakia.

Marek Matulka

May 28, 2016
Tweet

More Decks by Marek Matulka

Other Decks in Programming

Transcript

  1. Requirement CMS - manage corporate website - manage other brands’

    websites - build seasonal microsites - integrate with Magento store
  2. Why Drupal? - open source CMS - easy to install

    and manage - most required functionality available out of the box - easy to extend
  3. In order to create a good software, you have to

    know what the software is all about. – Eric Evans
  4. Ubiquitous Language Ubiquitous Language Domain Expert Analyst Tester Developer Developer

    Application Code Acceptance Criteria Specs and documentation
  5. 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
  6. 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
  7. Domain namespace Acme\Depot; interface DepotLocator { /** * @param string

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

    public function locateDepot($postcode) { return $this->entityMapper->mapDepot( $this->clientAdapter->findDepotByPostcode($postcode) );
  9. Layered Architecture “High level modules should not depend on lower

    level implementation” – Robert C. Martin User Interface Application Domain Infrastructure
  10. Clean Architecture Entities Use Cases Controllers Web Framework Drivers Interface

    Adapters Application Business Rules Enterprise Business Rules
  11. Why clean architecture? - your code is clean - decoupled

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

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

    centric domains (CRUD, REST) - prototyping
  14. Kernel public function boot() { $this->container = new ContainerBuilder(); $loader

    = new YamlFileLoader($this->container, new FileLocator('app/config')); $loader->load('parameters.yml'); $this->container->compile(); }
  15. 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(); }
  16. 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(); }
  17. 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; }
  18. 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; } }
  19. 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; } }
  20. 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/
  21. 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); } }
  22. 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) ); } }
  23. 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();
  24. 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)); } }
  25. Why write XML? - fetching data from Magento and dumping

    it into xml file is quick - xml can be imported by Drupal’s feed importer
  26. 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!
  27. 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); } } }
  28. Why direct save to the db? - Drupal promises data

    structure won’t ever change - It’s fast - 40k products in 2 minutes - It works!
  29. 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" },
  30. 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' => '', ], ], ];