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

Mission (Im)possible: Quality Decoupled Code with Drupal 7 (DrupalCamp Scotland)

Mission (Im)possible: Quality Decoupled Code with Drupal 7 (DrupalCamp Scotland)

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.

Marek Matulka

November 07, 2015
Tweet

More Decks by Marek Matulka

Other Decks in Programming

Transcript

  1. Marek Matulka

    View Slide

  2. at
    @super_marek

    View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. http://photobucket.com/user/marcrobinsone/media/Drupal%20Comics/marcrobinsonegmailcom__drupalNinja.png.html

    View Slide

  8. View Slide

  9. View Slide

  10. In order to create a good
    software, you have to know
    what the software is all
    about.
    – Eric Evans

    View Slide

  11. How do we learn?

    View Slide

  12. Stakeholders

    View Slide

  13. Ask for examples...
    ...write stories!

    View Slide

  14. Vocabulary

    View Slide

  15. Ubiquitous Language
    Ubiquitous
    Language

    View Slide

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

    View Slide

  17. Ubiquitous Language
    Ubiquitous
    Language
    Domain
    Expert
    Analyst
    Tester Developer
    Developer
    Application
    Code Acceptance
    Criteria
    Specs and
    documentation

    View Slide

  18. Example?

    View Slide

  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

    View Slide

  20. 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

    View Slide

  21. Domain
    namespace Acme\Depot;
    interface DepotLocator
    {
    /**
    * @param string $postcode
    *
    * @return Depot
    */
    public function locateDepot($postcode);
    }

    View Slide

  22. Implementation
    namespace Acme\MagentoAdapter;
    use Acme\Depot\DepotLocator;
    class MagentoDepotLocator implements DepotLocator
    {
    public function locateDepot($postcode)
    {
    return $this->entityMapper->mapDepot(
    $this->clientAdapter->findDepotByPostcode($postcode)
    );

    View Slide

  23. why interface?

    View Slide

  24. Layered Architecture
    “High level modules
    should not depend on
    lower level
    implementation”
    – Robert C. Martin
    User Interface
    Application
    Domain
    Infrastructure

    View Slide

  25. Dependency Inversion
    Policy Layer

    Policy Service
    Mechanism
    Layer

    Mechanism
    Service
    Utility Layer

    View Slide

  26. Layered Architecture
    User Interface
    Application
    Domain
    Infrastructure

    View Slide

  27. Layered Architecture
    User Interface
    Application
    Domain
    Infrastructure

    View Slide

  28. Clean Architecture
    Entities
    Use Cases
    Controllers
    Web

    View Slide

  29. Clean Architecture
    Entities
    Use Cases
    Controllers
    Web
    Framework
    Drivers
    Interface
    Adapters
    Application
    Business
    Rules
    Enterprise
    Business
    Rules

    View Slide

  30. Hexagonal Architecture
    UI Adapter
    Log Adapter
    Data Storage
    Adapter
    External Data
    Adapter
    Application
    Domain

    View Slide

  31. Hexagonal Architecture
    Acme\Depot\DepotLocator
    Acme\MagentoAdapter\MagentoDepotLocator

    View Slide

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

    View Slide

  33. Why clean architecture?

    View Slide

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

    View Slide

  35. View Slide

  36. View Slide

  37. symfony/dependency-injection
    symfony/config
    dependency injection

    View Slide

  38. Dependency Injection
    - a standardised and centralised way of
    constructing objects in our application

    View Slide

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

    View Slide

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

    View Slide

  41. 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();
    }

    View Slide

  42. 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();
    }

    View Slide

  43. 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;
    }

    View Slide

  44. 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;
    }
    }

    View Slide

  45. 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;
    }
    }

    View Slide

  46. 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/

    View Slide

  47. services.xml

    View Slide

  48. symfony/console
    console commands

    View Slide

  49. 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);
    }
    }

    View Slide

  50. 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)
    );
    }
    }

    View Slide

  51. app/console
    #!/usr/bin/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();

    View Slide

  52. symfony/filesystem
    persisting products

    View Slide

  53. 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));
    }
    }

    View Slide

  54. Why write XML?
    - fetching data from Magento and dumping
    it into xml file is quick
    - xml can be imported by Drupal’s feed
    importer

    View Slide

  55. 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!

    View Slide

  56. 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);
    }
    }
    }

    View Slide

  57. Why direct save to the db?
    - Drupal promises data structure won’t ever
    change
    - It’s fast - 40k products in 2 minutes
    - It works!

    View Slide

  58. 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"
    },

    View Slide

  59. container.php
    require_once DRUPAL_ROOT . '/../vendor/autoload.php';
    $kernel = new \Acme\Application\Kernel();
    $kernel->boot();
    variable_set('di_container', = $kernel->getContainer());

    View Slide

  60. 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' => '',
    ],
    ],
    ];

    View Slide

  61. settings.php
    ...
    include_once 'config/container.php';
    include 'database.php';
    ...

    View Slide

  62. 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];
    }

    View Slide

  63. Project folder structure

    View Slide

  64. View Slide

  65. View Slide

  66. as a CMS platform
    Drupal is great!

    View Slide

  67. Cherry pick what you need!
    Symfony Components

    View Slide

  68. Try it yourself!
    Decoupled code

    View Slide

  69. Right tool
    Choose wisely!

    View Slide

  70. View Slide

  71. https://speakerdeck.com/super_marek
    @super_marek

    View Slide