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

Mission (Im)possible: Quality Decoupled Code with Drupal 7 (PHP SW meetup)

Mission (Im)possible: Quality Decoupled Code with Drupal 7 (PHP SW meetup)

We're all excited about Drupal 8 coming up soon and we want to be ready for it! How can we make sure our code is ready for it? How can we build modules that are backwards compatible with D7 and future proofed for D8?

Learn how to build code decoupled from your platform and use it with Drupal or any other framework.

Presented at PHP SW meetup in Bristol, UK.

2bd48651cd01e0ca2e0a255a63da77aa?s=128

Marek Matulka

April 13, 2016
Tweet

Transcript

  1. 13 Apr 2016 By Marek Matulka

  2. at @super_marek

  3. None
  4. None
  5. None
  6. http://photobucket.com/user/marcrobinsone/media/Drupal%20Comics/marcrobinsonegmailcom__drupalNinja.png.html

  7. None
  8. None
  9. In order to create a good software, you have to

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

  11. Stakeholders

  12. Ask for examples... ...write stories!

  13. Vocabulary

  14. Ubiquitous Language Ubiquitous Language

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

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

    Application Code Acceptance Criteria Specs and documentation
  17. Example?

  18. 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
  19. 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
  20. Domain namespace Acme\Depot; interface DepotLocator { /** * @param string

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

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

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

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

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

  26. Layered Architecture User Interface Application Domain Infrastructure

  27. Hexagonal Architecture UI Adapter Log Adapter Data Storage Adapter External

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

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

  30. Why clean architecture?

  31. Why clean architecture? - your code is clean(er)

  32. Why clean architecture? - your code is clean(er) - decoupled

    from framework
  33. Why clean architecture? - your code is clean(er) - decoupled

    from framework - reusable
  34. Why clean architecture? - your code is clean(er) - decoupled

    from framework - reusable - easy to test
  35. Why clean architecture? - your code is clean(er) - decoupled

    from framework - reusable - easy to test - easy to maintain
  36. Why clean architecture? - your code is clean(er) - decoupled

    from framework - reusable - easy to test - easy to maintain - easy to change
  37. None
  38. None
  39. symfony/dependency-injection symfony/config dependency injection

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

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

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

    = new YamlFileLoader($this->container, new FileLocator('app/config')); $loader->load('parameters.yml'); $this->container->compile(); }
  43. 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(); }
  44. 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(); }
  45. 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; }
  46. 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; } }
  47. 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; } }
  48. 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/
  49. services.xml

  50. symfony/console console commands

  51. 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); } }
  52. 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) ); } }
  53. 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();
  54. symfony/filesystem persisting products

  55. 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)); } }
  56. Why write XML? - fetching data from Magento and dumping

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

    structure won’t ever change - It’s fast - 40k products in 2 minutes - It works!
  60. 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" },
  61. container.php <?php require_once DRUPAL_ROOT . '/../vendor/autoload.php'; $kernel = new \Acme\Application\Kernel();

    $kernel->boot(); variable_set('di_container', = $kernel->getContainer());
  62. 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' => '', ], ], ];
  63. settings.php <?php ... include_once 'config/container.php'; include 'database.php'; ...

  64. 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]; }
  65. Project folder structure

  66. None
  67. None
  68. as a CMS platform Drupal is great!

  69. Cherry pick what you need! Symfony Components

  70. Try it yourself! Decoupled code

  71. Right tool Choose wisely!

  72. joind.in/talk/7ea69 @super_marek