Coding for Drupal 8/9

Best practices and guidelines

Best practices

Some easy things
to keep in mind

Procedural code


                /**
                 * Literally returns shit.
                 *
                 * @return string
                 *   Guess what?!
                 */
                function _my_module_helper_function() {
                  return 'shit';
                }
              

Wherever possible, helper methods and custom logic should be bundled in dedicated service class(es) instead!

Always encapsulate related functionality in a class.

Make custom functionality extendable/swappable.

Complex logic in hooks/preprocesses


                /**
                 * Implements template_preprocess_HOOK().
                 */
                function my_theme_preprocess_whatever(&$variables) {
                  if ($variables['this'] === $variables['that']) {
                    $variables['this'] = 'is that';
                    $variables['that'] = 'is this';

                    $variables['this_and_that'] = [
                      $variables['this'],
                      $variables['that'],
                    ];
                  }
                  else {
                    $variables['that'] = 'not this';

                    foreach ($variables as $key => $variable) {
                      $variables[$key] = $variable;
                    }
                  }

                  // ...and a lot of more code!
                }
              

Wherever possible, complex logic should be bundled in dedicated service class(es) instead!

More readable code

Improves reusability of code - Keep it DRY!

Don't repeat yourself (DRY)


                /**
                 * Implements template_preprocess_HOOK() for node--article.html.twig.
                 */
                function my_theme_preprocess_node__article(&$variables) {
                  $variables['crap'] = TRUE;
                }

                /**
                 * Implements template_preprocess_HOOK() for node--page.html.twig.
                 */
                function my_theme_preprocess_node__page(&$variables) {
                  $variables['crap'] = TRUE;
                }

                /**
                 * Implements template_preprocess_HOOK() for node--event.html.twig.
                 */
                function my_theme_preprocess_node__event(&$variables) {
                  my_theme_preprocess_node__page($variables);
                }
              

                /**
                 * Implements template_preprocess_HOOK() for node--article.html.twig.
                 */
                function my_theme_preprocess_node__article(&$variables) {
                  _my_theme_make_it_crappy($variables);
                }

                /**
                 * Implements template_preprocess_HOOK() for node--page.html.twig.
                 */
                function my_theme_preprocess_node__page(&$variables) {
                  _my_theme_make_it_crappy($variables);
                }

                /**
                 * Implements template_preprocess_HOOK() for node--event.html.twig.
                 */
                function my_theme_preprocess_node__event(&$variables) {
                  _my_theme_make_it_crappy($variables);
                }

                /**
                 * Make it crappy.
                 */
                function _my_theme_make_it_crappy(&$variables) {
                  $variables['crap'] = TRUE;
                }
              

Code that is used more than once should be wrapped in its own method / function!

Do NOT reuse preprocess functions to avoid unwanted side effects

Early returns


                /**
                 * My awesome function.
                 *
                 * @param string|null $name
                 *   A name to extend.
                 */
                function my_awesome_function($name = NULL) {
                  if (!$name) {
                    return;
                  }

                  return $name . " <- I'm with stupid!";
                }
              

                /**
                 * Implements hook_preprocess_HOOK() for field.html.twig.
                 */
                function my_theme_preprocess_field(&$variables) {
                  $field_name = $variables['element']['#field_name'];

                  if ($field_name === 'field_1') {
                    // My custom logic for 'field_1'.
                    foreach ($variables['items'] as $key => &$item) {
                      if ($item['content']['#url'] instanceof Url) {
                        if (isset($item['content']['#options'])) {
                          $item['content']['#url'] = NULL;
                        }

                        $url = $item['content']['#url']->toString();
                      }
                      else {
                        $url = $item['content']['#url'];
                      }
                    }
                  }
                  elseif ($field_name === 'field_2') {
                    // My custom logic for 'field_2'.
                    foreach ($variables['items'] as $key => &$item) {
                      $item['url'] = $item['content']['#url']->toString();
                    }
                  }
                  elseif ($field_name === 'field_3') {
                    // My custom logic for 'field_3'.
                    foreach ($variables['items'] as $key => &$item) {
                      $item['test'] = $item['content']['#content'];
                    }
                  }
                  elseif ($field_name === 'field_4') {
                    // My custom logic for 'field_4'.
                    foreach ($variables['items'] as $key => &$item) {
                      $item['toast'] = $item['content']['#content'];
                    }
                  }
                  elseif ($field_name === 'field_5') {
                    // My custom logic for 'field_5'.
                    foreach ($variables['items'] as $key => &$item) {
                      if (!empty($item['content'])) {
                        return;
                      }
                    }
                  }
                  elseif ($field_name === 'field_6') {
                    // My custom logic for 'field_6'.
                    foreach ($variables['items'] as $key => &$item) {
                      if ($item['content']['#url'] instanceof Url) {
                        if (isset($item['content']['#options'])) {
                          $item['content']['#url'] = NULL;
                        }

                        $url = $item['content']['#url']->toString();
                      }
                      else {
                        $url = $item['content']['#url'];
                      }
                    }
                  }
                  elseif ($field_name === 'field_7') {
                    // My custom logic for 'field_7'.
                    foreach ($variables['items'] as $key => &$item) {
                      $item['toast'] = $item['content']['#content'];
                    }
                  }
                  elseif ($field_name === 'field_8') {
                    // My custom logic for 'field_8'.
                    foreach ($variables['items'] as $key => &$item) {
                      $item['url'] = $item['content']['#url']->toString();
                    }
                  }
                  elseif ($field_name === 'field_9') {
                    // My custom logic for 'field_9'.
                  }
                  elseif ($field_name === 'field_10') {
                    // My custom logic for 'field_10'.
                    foreach ($variables['items'] as $key => &$item) {
                      $item['toast'] = $item['content']['#content'];
                    }
                  }

                  // !!! IMPORTANT !!!
                  // This code should be executed for every goddamn field!!
                  $variables['crap'] = TRUE;
                }
              

                /**
                 * Implements hook_preprocess_HOOK() for field.html.twig.
                 */
                function my_theme_preprocess_field(&$variables) {
                  $field_name = $variables['element']['#field_name'];

                  if ($field_name === 'field_1') {
                    // My custom logic for 'field_1'.
                    foreach ($variables['items'] as $key => &$item) {
                      if ($item['content']['#url'] instanceof Url) {
                        if (isset($item['content']['#options'])) {
                          $item['content']['#url'] = NULL;
                        }

                        $url = $item['content']['#url']->toString();
                      }
                      else {
                        $url = $item['content']['#url'];
                      }
                    }
                  }
                  elseif ($field_name === 'field_2') {
                    // My custom logic for 'field_2'.
                    foreach ($variables['items'] as $key => &$item) {
                      $item['url'] = $item['content']['#url']->toString();
                    }
                  }
                  elseif ($field_name === 'field_3') {
                    // My custom logic for 'field_3'.
                    foreach ($variables['items'] as $key => &$item) {
                      $item['test'] = $item['content']['#content'];
                    }
                  }
                  elseif ($field_name === 'field_4') {
                    // My custom logic for 'field_4'.
                    foreach ($variables['items'] as $key => &$item) {
                      $item['toast'] = $item['content']['#content'];
                    }
                  }
                  elseif ($field_name === 'field_5') {
                    // My custom logic for 'field_5'.
                    foreach ($variables['items'] as $key => &$item) {
                      if (!empty($item['content'])) {
                        return;
                      }
                    }
                  }
                  elseif ($field_name === 'field_6') {
                    // My custom logic for 'field_6'.
                    foreach ($variables['items'] as $key => &$item) {
                      if ($item['content']['#url'] instanceof Url) {
                        if (isset($item['content']['#options'])) {
                          $item['content']['#url'] = NULL;
                        }

                        $url = $item['content']['#url']->toString();
                      }
                      else {
                        $url = $item['content']['#url'];
                      }
                    }
                  }
                  elseif ($field_name === 'field_7') {
                    // My custom logic for 'field_7'.
                    foreach ($variables['items'] as $key => &$item) {
                      $item['toast'] = $item['content']['#content'];
                    }
                  }
                  elseif ($field_name === 'field_8') {
                    // My custom logic for 'field_8'.
                    foreach ($variables['items'] as $key => &$item) {
                      $item['url'] = $item['content']['#url']->toString();
                    }
                  }
                  elseif ($field_name === 'field_9') {
                    // My custom logic for 'field_9'.
                  }
                  elseif ($field_name === 'field_10') {
                    // My custom logic for 'field_10'.
                    foreach ($variables['items'] as $key => &$item) {
                      $item['toast'] = $item['content']['#content'];
                    }
                  }

                  // !!! IMPORTANT !!!
                  // This code should be executed for every goddamn field!!
                  $variables['crap'] = TRUE;
                }
              

Avoid early returns in long and multi-purpose functions wherever possible - they will be overlooked!

Interfaces


                /**
                 * Provides my custom service functionality.
                 */
                class MyCustomService {

                }
              

                /**
                 * Provides my custom service functionality.
                 */
                class MyCustomService implements MyCustomServiceInterface {

                }
              

ALWAYS add an interface to your custom service class(es)!

Declare all public methods without having to define how these are implemented.

Declare any required constant values.

Should aways be used for variable typing instead of the service class itself.

Act as contract for the implementing classes and ensures that services are really interchangeable/swappable/overridable.

Error-prone code


                /**
                 * Implements hook_preprocess_HOOK() for node.html.twig
                 */
                function my_theme_preprocess_node(&$variables) {
                  /** @var \Drupal\node\NodeInterface $node */
                  $node = $variables['node'];

                  $value = $node->field_name->value;
                }
              

A long time later, customer says:

Hey, let's just remove that obsolete field_name field...

                /**
                 * Implements hook_preprocess_HOOK() for node.html.twig
                 */
                function my_theme_preprocess_node(&$variables) {
                  /** @var \Drupal\node\NodeInterface $node */
                  $node = $variables['node'];

                  if ($node->hasField('field_name')) {
                    $value = $node->get('field_name')->value;
                  }
                }
              

Always check things in code before using them!

Use API methods wherever possible!

Naming things is hard


                function whatever(array $arg = []) {
                  $my_string = 123.45;

                  $a = 'should have a variable name like "title"';

                  $articles = \Drupal::service('entity_type.manager')
                    ->getStorage('node')
                    ->loadMultiple($ids);
                }
              

                function my_node_loader(array $ids = []) {
                  $my_float = 123.45;

                  $title = 'should have a variable name like "title"';

                  $nodes = \Drupal::service('entity_type.manager')
                    ->getStorage('node')
                    ->loadMultiple($ids);
                }
              

Always use descriptive names for variables, functions, classes etc.

Do not worry too much about length - your IDE's autocomplete will take care of longer names!

Document all the things


                /**
                 * Returns nodes.
                 *
                 * @return array
                 *   The nodes.
                 */
                function getNodes() {
                  $nodes = \Drupal::service('entity_type.manager')
                    ->getStorage('node')
                    ->loadMultiple();

                  // Remove node 12.
                  unset($nodes[12]);

                  return $nodes;
                }
              

                /**
                 * Returns a list of all nodes.
                 *
                 * @return \Drupal\node\NodeInteface[]
                 *   An array of node objects keyed by the node ID.
                 */
                function getNodes() {
                  $nodes = \Drupal::service('entity_type.manager')
                    ->getStorage('node')
                    ->loadMultiple();

                  // @todo Make nodes to remove configurable.
                  // Remove node 12 due to requirements from ticket
                  // XYZ-1234 or any other details worth to note.
                  unset($nodes[12]);

                  return $nodes;
                }
              

Provide as much information as possible when documenting code.

Developers should be able to follow along by only reading those comments.

Variable typing


                $service = \Drupal::service('whatever.service');
                $service->doSomething();
              

                /** @var \Drupal\my_module\ServiceInterface $service */
                $service = \Drupal::service('whatever.service');
                $service->doSomething();
              

Drastically improves developer experience!

More readable code

Autocomplete for variable's methods, properties etc.

Improves IDE's refactoring capabilities

Variable typing

Random stuff
...but worth to note!

Always have a look at Drupal's core code first,
to learn how similar things are solved there!

Always prefer the system's APIs (plugins, services, subscribers, utilities etc.) instead of custom code and dirty hacks in preprocessors or alter hooks!

Better write more custom modules than less!

Do not throw all custom code in a single module. Otherwise have fun separating that later...

(Separation of concerns)

Keep the dependency list of your custom module(s) up to date!

Do not forget to create schema definitions for custom configuration in modules / themes.

When refactoring,
actually make code better than worse!

Avoid code scaffolding tools!
You may end up using outdated code,
besides it won't teach you anything.

Write your own code to understand and learn!

Questions?

Plugins vs. Services

When to use what?!

Plugins

Plugins implement different behaviors via a common interface.

Use plugins if a behavior needs to be selected and/or configured by the user.

Click here for more information...

Plugin example

Image effects

Act in the same way on the same data - However, each effect is very different

More plugin examples

  • Blocks
  • Conditions
  • Field types / formatters / widgets
  • Layouts
  • Menu links / Local tasks / Local actions

Tagged services

Tagged services implement different behaviors via a common interface.

Use tagged services instead of plugins, if there's no need for user interaction.

Click here for more information...

Tagged service example

Twig extension

Allows to extend Twig in a standardized way via tagging a service as Twig extension, but does not allow any configuration.

More tagged service examples

  • Access checks
  • Cache bins / contexts
  • Event subscribers
  • Theme negotiators
  • Stream wrappers

Services

Services provide the same functionality, and are interchangeable/swappable/overridable, differing only in their internal implementation.

Click here for more information...

Service example

Cache backend

A cache should provide get, set, and expire methods. The user just expects a cache, and one should be able to replace another without any functional difference. The internal implementation of those methods and the mechanisms it uses to do so can be wildly different.

More service examples

  • Date formatter
  • Entity type manager
  • Module / theme handler
  • Plugin managers
  • Renderer

Questions?

Time for a break

...and the best is yet to come!

Creating a custom plugin system

...a short wrap-up!

Plugin discovery options

  • Static
  • Hook
  • Annotations
    (combine with AnnotationBridgeDecorator)
  • YAML file(s)
  • ...create your own!

Required code

Plugin base class


                  class MyPlugin extends PluginBase implements MyPluginInterface {
                    // Your plugin base code...
                  }
                

Plugin definition class


                  class MyPluginDefinition extends PluginDefinition implements MyPluginDefinitionInterface {

                    /**
                     * The description.
                     *
                     * @var string
                     */
                    protected $description;

                    /**
                     * The human-readable name.
                     *
                     * @var string
                     */
                    protected $label;

                    /**
                     * Constructs a new MyPluginDefinition object.
                     *
                     * @param array $definition
                     *   An array of values from the annotation.
                     */
                    public function __construct(array $definition) {
                      foreach ($definition as $property => $value) {
                        $this->set($property, $value);
                      }
                    }

                    /**
                     * Gets any arbitrary property.
                     *
                     * @param string $property
                     *   The property to retrieve.
                     *
                     * @return mixed
                     *   The value for that property, or NULL if the property does not exist.
                     */
                    public function get($property) {
                      if (property_exists($this, $property)) {
                        $value = isset($this->{$property}) ? $this->{$property} : NULL;
                      }

                      return $value;
                    }

                    /**
                     * Returns the localized description.
                     *
                     * @return string
                     *   The localized description of the EasyMDE editor item definition.
                     */
                    public function getDescription() {
                      return $this->description;
                    }

                    /**
                     * Returns the localized human-readable name.
                     *
                     * @return string
                     *   The localized human-readable label of the EasyMDE editor item definition.
                     */
                    public function getLabel() {
                      return $this->label;
                    }

                    /**
                     * Sets a value to an arbitrary property.
                     *
                     * @param string $property
                     *   The property to use for the value.
                     * @param mixed $value
                     *   The value to set.
                     *
                     * @return static
                     */
                    public function set($property, $value) {
                      if (property_exists($this, $property)) {
                        $this->{$property} = $value;
                      }

                      return $this;
                    }
                  }
                

Plugin annotation class


                  class MyPluginAnnotation extends Plugin {

                    /**
                     * The plugin class.
                     *
                     * This default value is used for plugins
                     * defined in YAML files that do not specify
                     * a class themselves.
                     *
                     * @var string
                     */
                    public $class = MyPlugin::class;

                    /**
                     * The description of plugin.
                     *
                     * @ingroup plugin_translatable
                     *
                     * @var \Drupal\Core\Annotation\Translation
                     */
                    public $description;

                    /**
                     * The plugin ID.
                     *
                     * @var string
                     */
                    public $id;

                    /**
                     * The human-readable name of the plugin.
                     *
                     * @ingroup plugin_translatable
                     *
                     * @var \Drupal\Core\Annotation\Translation
                     */
                    public $label;

                  }
                
...as long as you go with annotated class discovery

Example plugin class


                  /**
                   * My custom plugin.
                   *
                   * @MyPluginAnnotation(
                   *   id = "my_plugin_id",
                   *   label = "My plugin label",
                   *   description = "Provides my custom plugin.",
                   * )
                   */
                  class MyCustomPlugin extends MyPlugin {
                    // Your custom plugin code...
                  }
                
...for annotated class discovery

Plugin manager service definition


                  services:
                    plugin.manager.my_plugin:
                      class: Drupal\my_module\MyPluginManager
                      parent: default_plugin_manager
                

Plugin manager class


                  class MyPluginManager extends DefaultPluginManager implements MyPluginManagerInterface {

                    /**
                     * Constructs a MyPluginManager object.
                     *
                     * @param \Traversable $namespaces
                     *   An object that implements \Traversable which contains the root paths
                     *   keyed by the corresponding namespace to look for plugin implementations.
                     * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
                     *   Cache backend instance to use.
                     * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
                     *   The module handler to invoke the alter hook with.
                     */
                    public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
                      parent::__construct(
                        // The subdirectory where annotated class plugins will be searched for.
                        'Plugin/MyPlugin',
                        $namespaces,
                        $module_handler,
                        // The interface each plugin should implement.
                        MyPluginInterface::class,
                        // The plugin annotation class name.
                        MyPluginAnnotation::class
                      );

                      // Provide a hook to allow altering plugin definitions.
                      $this->alterInfo('my_plugin_info');

                      // Set up cache backend.
                      $this->setCacheBackend($cache_backend, 'my_plugin_info_plugins');
                    }

                    /**
                     * {@inheritdoc}
                     */
                    protected function getDiscovery() {
                      if (!$this->discovery) {
                        // Use annotated class discovery.
                        $discovery = new AnnotatedClassDiscovery(
                          $this->subdir,
                          $this->namespaces,
                          $this->pluginDefinitionAnnotationName,
                          $this->additionalAnnotationNamespaces
                        );

                        // Use YAML-based discovery.
                        $discovery = new YamlDiscoveryDecorator(
                          $discovery,
                          'my_plugin',
                          $this->moduleHandler->getModuleDirectories() + $this->themeHandler->getThemeDirectories()
                        );
                        $discovery
                          ->addTranslatableProperty('label')
                          ->addTranslatableProperty('description');

                        // Ensure that all definitions are run through the annotation process.
                        $discovery = new AnnotationBridgeDecorator($discovery, $this->pluginDefinitionAnnotationName);

                        // Injects dependencies into derivers if they use ContainerDeriverInterface.
                        $discovery = new ContainerDerivativeDiscoveryDecorator($discovery);

                        $this->discovery = $discovery;
                      }

                      return $this->discovery;
                    }

                    /**
                     * {@inheritdoc}
                     */
                    public function processDefinition(&$definition, $plugin_id) {
                      parent::processDefinition($definition, $plugin_id);

                      // Ensure definition class.
                      if (!$definition instanceof MyPluginDefinitionInterface) {
                        throw new InvalidPluginDefinitionException(
                          $plugin_id,
                          sprintf('The "%s" plugin definition must implement %s', $plugin_id, MyPluginDefinitionInterface::class)
                        );
                      }
                    }

                  }
                

...and don't forget about the corresponding interfaces!

(At least for plugin, definition and manager)

"Special" plugin manager implementations

You have to categorize plugin definitions?

Implement CategorizingPluginManagerInterface!

Example: Field type manager

You need a fallback plugin behavior?

Implement FallbackPluginManagerInterface!

Example: Views plugin managers

You need to allow filtering definitions?

Implement FilteredPluginManagerInterface!

Example: Block/Layout managers

Questions?

Defining a custom service

...a short wrap-up!

Before you start

Check existing services for similar functionality/reusability!

Better extend, wrap or decorate an existing service instead of creating your own.

Required code

Service definition file


                  services:
                    my_service:
                      class: \Drupal\my_namespace\MyService
                    my_other_service:
                      class: \Drupal\my_namespace\MyOtherService
                      arguments: ['@entity_type.manager']
                    one_more_service:
                      class: \Drupal\my_namespace\OneMoreService
                      tags:
                        - { name: my.tag }
                

Service class


                  class MyService implements MyServiceInterface {
                    // Your service code...
                  }
                

Service interface


                  interface MyServiceInterface {
                    // Your service interface code...
                  }
                

Always provide an interface!

Even if empty, this is required for a service
to really be interchangeable/swappable/overridable.

Do one thing
and one thing only!

Dependency injection


                class MyService implements MyServiceInterface {

                  /**
                   * {@inheritdoc}
                   */
                  public function doSomething() {
                    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
                    $entity_type_manager = \Drupal::service('entity_type.manager');

                    $storage = $entity_type_manager->getStorage('node');
                  }

                }
              
my_module.services.yml

                  services:
                    my_service:
                      class: \Drupal\my_namespace\MyService
                      arguments: ['@entity_type.manager']
                
MyService.php

                  class MyService implements MyServiceInterface {

                    /**
                     * The entity type manager.
                     *
                     * @var \Drupal\Core\Entity\EntityTypeManagerInterface
                     */
                    protected $entityTypeManager;

                    /**
                     * Constructs a new MyService object.
                     *
                     * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
                     *   The entity type manager.
                     */
                    public function __construct(EntityTypeManagerInterface $entity_type_manager) {
                      $this->entityTypeManager = $entity_type_manager;
                    }

                    /**
                     * {@inheritdoc}
                     */
                    public function doSomething() {
                      $storage = $this->entityTypeManager->getStorage('node');
                    }

                  }
                

All required services should be injected instead of being instantiated directly wherever possible!

This pattern is not limited to services alone.
Also use this for/in:

  • Controller class(es)
  • Form class(es)
  • Plugin class(es)
    (e.g. by implementing ContainerFactoryPluginInterface)
  • ...any other object that provides a create() factory method

Questions?

Final wise words

Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.

Martin Golding

Thank you