A Practical Introduction to

OOPing in Drupal

Geoff Appleby

Pacific Northwest Drupal Summit 2017

Boilerplate

  • ga.info.yml
  • ga.routing.yml
  • ga.links.menu.yml
  • ga.libraries.yml
  • ga.services.yml
  • config/install/ga.settings.yml
  • config/schema/ga.schema.yml

Drupal Console

DrupalConsole.com

Google Analytics Command Queue


          ga('create', 'UA-12345678-1');
          ga('set', 'dimension1', 'value');
          ga('send', 'pageview');
        

Google Analytics Command Queue


          ga.apply(null, ['create', 'UA-12345678-1']);
          ga.apply(null, ['set', 'dimension1', 'value']);
          ga.apply(null, ['send', 'pageview']);
        

Google Analytics Command Queue


          var commands = [
            ['create', 'UA-12345678-1'],
            ['set', 'dimension1', 'value'],
            ['send', 'pageview'],
          ];

          for (var i = 0; i < commands.length; i++) {
            ga.apply(null, commands[i]);
          }
        

            (function (drupalSettings) {
              'use strict';

              window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;

              for (var i = 0; i < drupalSettings.ga.commands.length; i++) {
                ga.apply(null, drupalSettings.ga.commands[i]);
              }

            })(drupalSettings);
          
/js/analytics.js

            analytics:
              version: 8001
              js:
                js/analytics.js: {}
                https://www.google-analytics.com/analytics.js: {type: external, attributes: { async: true } }
              dependencies:
               - core/drupalSettings
          
/ga.libraries.yml

            namespace Drupal\ga\AnalyticsCommand;

            class Create {

            }
          
/src/AnalyticsCommand/Create.php

Namespaces & Autoloading

Drupal\ga\AnalyticsCommand\Create /src/AnalyticsCommand/Create.php

Namespaces & Autoloading

  • ga.info.yml
  • ga.module
  • src/
    • AnalyticsCommand/
      • Create.php
      • Set.php
      • Send.php

PSR-?


            namespace Drupal\ga\AnalyticsCommand;

            class Create {

              protected $trackingId;

            }
          
/src/AnalyticsCommand/Create.php

Visibility

  • private
  • protected
  • public

            namespace Drupal\ga\AnalyticsCommand;

            class Create {

              protected $trackingId;

              public function __construct($trackingId) {
                $this->trackingId = $trackingId;
              }

            }
          
/src/AnalyticsCommand/Create.php

            namespace Drupal\ga\AnalyticsCommand;

            class Create {

              protected $trackingId;

              public function __construct($trackingId) {
                $this->trackingId = $trackingId;
              }

              public function getSettingCommand() {
                return ['create', $this->trackingId];
              }

            }
          
/src/AnalyticsCommand/Create.php

             

            $createInstance = new \Drupal\ga\AnalyticsCommand\Create('UA-12345678-1');

            print_r($createInstance->getSettingCommand());
            //Array
            //(
            //    [0] => create
            //    [1] => UA-12345678-1
            //)
          

            use \Drupal\ga\AnalyticsCommand\Create;

            $createInstance = new Create('UA-12345678-1');

            print_r($createInstance->getSettingCommand());
            //Array
            //(
            //    [0] => create
            //    [1] => UA-12345678-1
            //)
          

            namespace Drupal\ga\AnalyticsCommand;

            class Send {

              protected $hitType;

              public function __construct($hitType) {
                $this->hitType = $hitType;
              }

              public function getSettingCommand() {
                return ['send', $this->hitType];
              }

            }
          
/src/AnalyticsCommand/Send.php

            namespace Drupal\ga\AnalyticsCommand;

            class Pageview extends Send {

            }
          
/src/AnalyticsCommand/Pageview.php

            namespace Drupal\ga\AnalyticsCommand;

            class Pageview extends Send {

              public function __construct() {
                parent::__construct('pageview');
              }

            }
          
/src/AnalyticsCommand/Pageview.php

            use Drupal\ga\AnalyticsCommand\Pageview;

            $pageviewInstance = new Pageview();

            print_r($pageviewInstance->getSettingCommand());
            //Array
            //(
            //    [0] => send
            //    [1] => pageview
            //)
          

            namespace Drupal\ga\AnalyticsCommand;

            class Set {

              protected $settingKey;
              protected $settingValue;

              public function __construct($settingKey, $settingValue) {
                $this->settingKey = $settingKey;
                $this->settingValue = $settingValue;
              }

              public function getSettingCommand() {
                return ['set', $this->settingKey, $this->settingValue];
              }

            }
          
/src/AnalyticsCommand/Set.php

            namespace Drupal\ga\AnalyticsCommand;

            class SetDimension extends Set {

              public function __construct($index, $value) {
                parent::__construct('dimension' . $index, $value);
              }

            }
          
/src/AnalyticsCommand/SetDimension.php

            namespace Drupal\ga\AnalyticsCommand;

            interface SettingItemInterface {

              public function getSettingCommand();

            }
          
/src/AnalyticsCommand/SettingItemInterface.php

            namespace Drupal\ga\AnalyticsCommand;

            class Send implements SettingItemInterface {

              protected $hitType;

              public function __construct($hitType) {
                $this->hitType = $hitType;
              }

              public function getSettingCommand() {
                return ['send', $this->hitType];
              }

            }
          
/src/AnalyticsCommand/Send.php

            namespace Drupal\ga\AnalyticsCommand;

            interface SettingItemInterface {

              public function getSettingCommand();

              public function getPriority();

            }
          
/src/AnalyticsCommand/SettingItemInterface.php

            namespace Drupal\ga\AnalyticsCommand;

            class Create implements SettingItemInterface {

              protected $trackingId;
              protected $priority;

              public function __construct($trackingId, $priority) {
                $this->trackingId = $trackingId;
                $this->priority = $priority;
              }

              public function getSettingCommand() {
                return ['create', $this->trackingId];
              }

              public function getPriority() {
                return $this->priority;
              }

            }
          
/src/AnalyticsCommand/Create.php

            namespace Drupal\ga\AnalyticsCommand;

            trait PrioritizedTrait {

              protected $priority;

              public function getPriority() {
                return $this->priority;
              }

            }
          
/src/AnalyticsCommand/PrioritizedTrait.php

            namespace Drupal\gap\AnalyticsCommand;

            class Create implements SettingItemInterface {

              use PrioritizedTrait;

              protected $trackingId;

              public function __construct($trackingId, $priority) {
                $this->trackingId = $trackingId;
                $this->priority = $priority;
              }

              public function getSettingCommand() {
                return ['create', $this->trackingId];
              }

            }
          
/src/AnalyticsCommand/Create.php

            namespace Drupal\gap\AnalyticsCommand;

            class Create implements SettingItemInterface {

              use PrioritizedTrait;

              const DEFAULT_PRIORITY = 300;

              protected $trackingId;

              public function __construct($trackingId, $priority = self::DEFAULT_PRIORITY) {
                $this->trackingId = $trackingId;
                $this->priority = $priority;
              }

              public function getSettingCommand() {
                return ['create', $this->trackingId];
              }

            }
          
/src/AnalyticsCommand/Create.php

            namespace Drupal\ga\Event;

            use Drupal\ga\AnalyticsCommand\SettingItemInterface;
            use Symfony\Component\EventDispatcher\Event;

            class CollectEvent extends Event {

              protected $commands;

              public function __construct() {
                $this->commands = [];
              }

              public function addCommand(SettingItemInterface $item) {
                $this->commands[] = $item;
              }

              public function getDrupalSettingCommands() {
                usort(
                  $this->commands,
                    function (SettingItemInterface $a, SettingItemInterface $b) {
                    return $b->getPriority() - $a->getPriority();
                  }
                );

                return array_map(
                  function (SettingItemInterface $item) {
                    return $item->getSettingCommand()
                  },
                  $this->commands
                );
              }

            }
          
/src/Event/CollectEvent.php

            use Drupal\ga\Event\CollectEvent;

            /**
             * Implements hook_page_attachments().
             */
            function ga_page_attachments(array &$attachments) {

              $event = \Drupal::service('event_dispatcher')
                ->dispatch('ga.collect', new CollectEvent());

              $attachments['#attached']['library'][] = 'ga/analytics';
              $attachments['#attached']['drupalSettings']['ga']['commands'] =
                $event->getDrupalSettingCommands();
            }
          
/ga.module

            namespace Drupal\ga\EventSubscriber;

            use Drupal\Core\Config\ConfigFactoryInterface;
            use Drupal\ga\AnalyticsCommand\Create;
            use Drupal\ga\AnalyticsCommand\Pageview;
            use Drupal\ga\Event\CollectEvent;
            use Symfony\Component\EventDispatcher\EventSubscriberInterface;

            class DefaultCommandSubscriber implements EventSubscriberInterface {

              protected $configFactory;

              public static function getSubscribedEvents() {
                return [
                  'ga.collect' => [
                    ['onCollectDefaultCommands'],
                  ],
                ];
              }

              public function __construct(ConfigFactoryInterface $configFactory) {
                $this->configFactory = $configFactory;
              }

              public function onCollectDefaultCommands(CollectEvent $event) {
                $config = $this->configFactory->get('ga.settings');
                $event->addCommand(new Create($config->get('tracking_id')));
                $event->addCommand(new Pageview());
              }

            }

          
/src/EventSubscriber/DefaultCommandSubscriber.php

            services:
              ga.default_command_subscriber:
                class: Drupal\ga\EventSubscriber\DefaultCommandSubscriber
                  arguments: ['@config.factory']
                tags:
                  - { name: event_subscriber }
          
/ga.services.yml

            namespace Drupal\my_module\EventSubscriber;

            use Drupal\Core\Routing\RouteMatchInterface;
            use Drupal\ga\AnalyticsCommand\SetDimension;
            use Drupal\ga\Event\CollectEvent;
            use Drupal\node\NodeInterface;
            use Drupal\taxonomy\TermInterface;
            use Symfony\Component\EventDispatcher\EventSubscriberInterface;

            class EntityAnalyticsSubscriber implements EventSubscriberInterface {

              protected $routeMatch;

              public static function getSubscribedEvents() {
                return [
                  'ga.collect' => [
                    ['onCollect'],
                  ],
                ];
              }

              public function __construct(RouteMatchInterface $routeMatch) {
                $this->routeMatch = $routeMatch;
              }

              public function onCollect(CollectEvent $event) {
                if (($node = $this->routeMatch->getParameter('node'))) {
                  $this->applyAttributesFromNode($event, $node);
                }
                elseif (($term = $this->routeMatch->getParameter('taxonomy_term'))) {
                  $this->applyAttributesFromTaxonomyTerm($event, $term);
                }
              }

              protected function applyAttributesFromNode(CollectEvent $event, NodeInterface $node) {
                if (!empty($node->field_category[0]->entity)) {
                  $category = $node->field_category[0]->entity->name->value;
                  $event->addCommand(new SetDimension(1, $category));
                }
              }

              protected function applyAttributesFromTaxonomyTerm(CollectEvent $event, TermInterface $term) {
                $event->addCommand(new SetDimension(1, $term->name->value));
              }
            }
          
/src/EventSubscriber/EntityAnalyticsSubscriber.php

            namespace Drupal\ga\AnalyticsCommand;

            class SetMetric extends Set {

              public function __construct($index, $value, $priority = self::DEFAULT_PRIORITY) {
                if (!is_int($index)) {
                  throw new \InvalidArgumentException("Metric index must be a positive integer.");
                }
                if (!is_numeric($value)) {
                  throw new \InvalidArgumentException("Metric value must be a number.");
                }

                parent::__construct('dimension' . $index, $value, $priority);
              }

            }
          
/src/AnalyticsCommand/SetMetric.php

            class EntityAnalyticsSubscriber implements EventSubscriberInterface {

              public function onCollect(CollectEvent $event) {
                try {
                  $event->addCommand(new SetMetric(1, 'string'));
                }
                catch (\InvalidArgumentException $exception) {
                  $this->logger->error($exception->getMessage());
                }
              }

            }
          

          namespace Drupal\ga\Form;

          use Drupal\Core\Form\ConfigFormBase;
          use Drupal\Core\Form\FormStateInterface;

          class AdminSettingsForm extends ConfigFormBase {

            public function getFormId() {
              return 'ga_admin_settings';
            }

            protected function getEditableConfigNames() {
              return ['ga.settings'];
            }

            public function buildForm(array $form, FormStateInterface $form_state) {
              $config = $this->config('ga.settings');

              $form['tracking_id'] = [
                '#type' => 'textfield',
                '#title' => $this->t('Web Property Tracking ID'),
                '#default_value' => $config->get('tracking_id'),
              ];

              return parent::buildForm($form, $form_state);
            }

            public function validateForm(array &$form, FormStateInterface $form_state) {
              parent::validateForm($form, $form_state);

              $property = $form_state->getValue('tracking_id');
              if (!empty($property) && !preg_match('/^UA-\d+-\d+$/', $property)) {
                $form_state->setErrorByName(
                  'tracking_id',
                  $this->t('The provided Tracking ID is not valid.')
                );
              }
            }

            public function submitForm(array &$form, FormStateInterface $form_state) {
              $this->config('ga.settings')
                ->set('tracking_id', $form_state->getValue('tracking_id'))
                ->save();

              parent::submitForm($form, $form_state);
            }

          }
        
/src/Form/AdminSettingsForm.php

            namespace Drupal\Tests\ga\Unit\AnalyticsCommand;

            use Drupal\ga\AnalyticsCommand\Create;
            use Drupal\Tests\UnitTestCase;

            class CreateTest extends UnitTestCase {

              public function testDefaultPriority() {
                $command = new Create('UA-12345678-1');

                $this->assertEquals(300, $command->getPriority());
              }

              public function testBasicSettingCommands() {
                $command = new Create('UA-12345678-1');

                $this->assertEquals(['create', 'UA-12345678-1'], $command->getSettingCommand());
              }

            }
          
/tests/src/Unit/AnalyticsCommand/CreateTest.php

            namespace Drupal\Tests\ga\Unit\Event;

            use Drupal\ga\Event\CollectEvent;
            use Drupal\Tests\UnitTestCase;

            class CollectEventTest extends UnitTestCase {

              private $event;

              public function setUp() {
                parent::setUp();

                $this->event = new CollectEvent();
              }

              public function testEmptyCollection() {
                $result = $this->event->getDrupalSettingCommands();
                $this->assertEquals([], $result);
              }

              public function testCreateSend() {
                $this->event->addCommand(new Create('UA-12345678-1'));
                $this->event->addCommand(new Pageview());

                $result = $this->event->getDrupalSettingCommands();
                $this->assertEquals(
                  [['create', 'UA-12345678-1'], ['send', 'pageview']],
                  $result
                );
              }

              public function testSendCreate() {
                $this->event->addCommand(new Pageview());
                $this->event->addCommand(new Create('UA-12345678-1'));

                $result = $this->event->getDrupalSettingCommands();
                $this->assertEquals(
                  [['create', 'UA-12345678-1'], ['send', 'pageview']],
                  $result
                );
              }

            }
          
/tests/src/Unit/AnalyticsCommand/CreateTest.php

            $ vendor/bin/phpunit --configuration core/phpunit.xml modules/ga
            Testing started at 11:33 PM ...
            PHPUnit 4.8.26 by Sebastian Bergmann and contributors.

            Time: 11.66 seconds, Memory: 6.00MB

            OK (71 tests, 117 assertions)