Mission Impossible: Symfony components in Drupal 7

Mission Impossible: Symfony components in Drupal 7

I am going to share my experience with introducing Symfony components into Drupal 7. Learn how to incorporate components like HttpKernel, DependencyInjection or Configuration to bring modern tools and practices into an outdated project.

2bd48651cd01e0ca2e0a255a63da77aa?s=128

Marek Matulka

July 04, 2015
Tweet

Transcript

  1. 3.

    at

  2. 4.
  3. 5.
  4. 6.
  5. 7.

    Background Magento platform: - old version - full of custom

    modifications - very basic CMS - limited budget
  6. 8.

    Requirement CMS - manage corporate website - manage other brands’

    websites - build seasonal microsites - integrate with Magento store
  7. 10.

    Why Drupal? - open source CMS - easy to install

    and manage - most required functionality available out of the box - easy to extend - easy to theme - fits the “limited budget” price tag
  8. 12.
  9. 13.
  10. 18.
  11. 19.
  12. 25.

    Dependency Injection Dependency Injection is where components are given their

    dependencies through their constructors, methods, or directly into fields.
  13. 26.

    Inject via constructor class Attendance { /** * @var Storage

    */ private $storage; public function __construct(Storage $storage) { $this->storage = $storage; } // ... }
  14. 27.

    Inject via setter class Attendance { /** * @var Storage

    */ private $storage; public function setStorage(Storage $storage) { $this->storage = $storage; } // ... }
  15. 28.

    Inject via public property class Attendance { /** * @var

    Storage */ public $storage; // ... } $attendance->storage = $storage;
  16. 29.

    Pros & cons of DI Pros: - clear definition of

    what objects are required - modular, decoupled architecture - makes testing code trivial Cons: - an overkill for small or short lived projects
  17. 30.

    Code Example class Mailer { public function sendEmail($subject, $body) {

    // Send email } } class Order { public function sendConfirmation() { $email = new Mailer(); $email->sendEmail($subject, $body); } }
  18. 31.

    The other way round class Order { /** * @var

    Mailer */ private $mailer; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function sendConfirmation() { $this->mailer->sendEmail($subject, $body); } }
  19. 32.

    Interfaces to the rescue! interface OrderConfirmationSender { public function sendMessage($subject,

    $body); } class EmailMessageService implements OrderConfirmationSender { public function sendMessage($subject, $body) { // send email } }
  20. 33.

    Interfaces to the rescue! interface OrderConfirmationSender { public function sendMessage($subject,

    $body); } class EmailMessageService implements OrderConfirmationSender { private $mailer; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function sendMessage($subject, $body) { $this->mailer->sendEmail($subject, $body); } }
  21. 34.

    Dependency Injection class Order { /** * @var OrderConfirmationSender */

    private $confirmationSender; public function __construct(OrderConfirmationSender $confirmationSender) { $this->confirmationSender = $confirmationSender; } public function sendConfirmation() { $this->confirmationSender->sendMessage($subject, $body); } }
  22. 39.

    Installing dependencies $ composer install Loading composer repositories with package

    information Installing dependencies (including require-dev) - Installing symfony/dependency-injection (v2.5.1) Loading from cache - Installing symfony/filesystem (v2.5.1) Loading from cache - Installing symfony/config (v2.5.1) Loading from cache symfony/dependency-injection suggests installing symfony/yaml () symfony/dependency-injection suggests installing symfony/proxy-manager-bridge (Generate service proxies to lazy load them) Writing lock file Generating autoload files $
  23. 40.

    Installing dependencies $ composer install - will install all dependencies

    as locked in composer. lock file (if exists) $ composer update - will attempt to update all dependencies to the latest versions as set in composer.json
  24. 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(); }
  25. 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(); }
  26. 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(); }
  27. 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; }
  28. 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; } }
  29. 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; } }
  30. 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/
  31. 51.

    Installing dependencies $ composer require symfony/console ~2.5 ./composer.json has been

    updated Loading composer repositories with package information Updating dependencies (including require-dev) - Installing symfony/console (v2.5.1) Loading from cache symfony/console suggests installing symfony/event-dispatcher () symfony/console suggests installing symfony/process () symfony/console suggests installing psr/log (For using the console logger) Writing lock file Generating autoload files $
  32. 53.

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

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

    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();
  35. 57.

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

    Why write XML? - fetching data from Magento and dumping

    it into xml file is quick - xml can be imported by Drupal’s feed importer
  37. 59.

    Why it didn’t work? - Feed importer was damn slow

    - 10k products imported in 4.5h - Scheduling import with Elysia Cron module was very flaky - Scheduled feed importer would add products all over again - resources hungry!
  38. 60.

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

    Why direct save to the db? - Drupal promises data

    structure won’t ever change - It’s fast - 40k products in 2 minutes - It works!
  40. 62.

    composer.json "require": { "php": ">=5.4.0", "symfony/dependency-injection": "~2.5", "symfony/config": "~2.5", "symfony/console":

    "~2.5", "drush/drush": "7.*@dev", "symfony/finder": "~2.5", "symfony/filesystem": "~2.6", "doctrine/dbal": "~2.5", "rhumsaa/uuid": "~2.8" },
  41. 64.

    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' => '', ], ], ];
  42. 68.
  43. 69.
  44. 75.