Nghiên cứu introduction to services, plugins and events in Drupal

30th Jun 2022
Table of contents

That being said, here’s an introduction to services, plugins and events in Drupal. Read on!

OOP: Object-Oriented Programming

Since Drupal 8, the paradigm for writing Drupal modules changed for the better. Instead of writing the old procedural code, now we work with classes using Object Oriented Programming (OOP), a programming paradigm that involves objects that interact with each other. It ultimately provides you with objects that extend other objects by using classes, interfaces and traits.

Let’s see in more detail what each one of these elements is:

  • Classes - They’re groups of methods or properties organized in a file offering a certain functionality. Each class can have use statements at the top and requires other classes, interfaces or traits to provide the service.
  • Interfaces - These are base templates implemented by classes. They define what methods and properties the class should have.
  • Traits - An alternative for reusing code, they allow classes that use them to access certain functions without duplicating the code. Probably the most common trait in Drupal is the StringTranslationTrait, which enables the use of the “t” function to translate strings.

In Drupal, every class you create must have a corresponding namespace. The namespaces work for package-based autoloading and use the standard PSR-4. For every Drupal component, the namespace will always start with \Drupal and the rest will depend on what type of element we're adding the class to, following the pattern described in the official documentation.

In Drupal, every class you create must have a corresponding namespace.

Services

In Drupal, services are objects living inside the service container. They are classes used to encapsulate a specific functionality. Almost everything we do in Drupal is tied to a service: for example, knowing the current user of the site \Drupal::currentUser(), the current path \Drupal::routeMatch(), accessing and altering nodes and other entities, generating UUIDs, managing path aliases, and so on.

Drupal provides many services in core, but we can also create our own custom services. For example, if you have a controller with a helper function and you realize you can use this helper function in another controller (or another class), you should move this function to a service and inject it into the classes that need it. If you’re writing a helper or reusable function in your .module file (a function that’s not a hook), consider doing it as a service, since that’s probably how it should be done.

You can create your own services by either creating a class that encapsulates the functionality needed or telling Drupal how to instantiate it from the service container. The custom service class doesn't need to extend any other class. 

The only mandatory aspect of creating a service is telling Drupal how to instantiate it. This is done in the file example.services.yml, where “example” is the name of your module. In the following example we can see the services file for the module download_files:

services:
  download_files.event_subscriber:
    class: Drupal\download_files\EventSubscriber\FileDownloadedEventSubscriber
    arguments: ['@logger.factory', '@entity_type.manager']
    tags:
      - {name: event_subscriber}

Here, we’re defining a service called download_files.event_subscriber, as well as telling Drupal which class provides the functionality and what are the arguments needed by the constructor of that class.

Dependency Injection

As far as services are concerned, dependency injection comes in handy. Dependency injection is an OOP concept in which you don’t instantiate the dependencies granularly. Instead, you "inject" them into the classes that need them. A basic example is having a __construct method in your class that receives the dependencies instead of instantiating them. In this case, the caller will be in charge of sending those dependencies.

With dependency injection in Drupal, you want the framework to set the dependencies, rather than sending them directly from your code. You can inject any existing service into any service or class that implements ContainerInjectionInterface (directly or through any of its parents).

Injecting Dependencies to a Service

To inject dependencies into your service, you will need to:

  1. Add arguments to service declaration under the key "arguments" and add the symbol "@" before the service name:
    services:
      download_files.event_subscriber:
        class: Drupal\download_files\EventSubscriber\FileDownloadedEventSubscriber
        arguments: ['@logger.factory', '@entity_type.manager']
        tags:
          - {name: event_subscriber}
  2. Update __construct() method to receive and store the injected services, like:
    public function __construct(LoggerChannelFactoryInterface $loggerFactory, EntityTypeManagerInterface $entity_type_manager) {
      $this->logger = $loggerFactory->get('download_files');
      $this->entityTypeManager = $entity_type_manager;
    }

In the previous example, we’re injecting the logger.factory and entity_type.manager services and storing them in the logger and entityTypeManager properties of the class.

By doing this, don’t mind about your class getting the dependencies: this is with Drupal now.

Injecting Dependencies to a Block

To inject a dependency in a Block class, you need to:

  1. Implement Drupal\Core\Plugin\ContainerFactoryPluginInterface in the class:
    class DownloadFilesBlock extends BlockBase implements ContainerFactoryPluginInterface
  2. Update __construct() method to receive and store the injected services:
    public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $form_builder) {
        parent::__construct($configuration, $plugin_id, $plugin_definition);
        $this->formBuilder = $form_builder;
      }
  3. Implement create() method and use the service container to pass the services needed by the constructor:
    public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
        return new static(
          $configuration,
          $plugin_id,
          $plugin_definition,
          $container->get('form_builder')
        );
      }

Injecting Dependencies to a Form

When you work with forms, they extend from the class FormBase which already implements Drupal\Core\DependencyInjection\ContainerInjectionInterface. So, in those cases, all you need to do to inject a dependency is:

  1. Update __construct() method to receive and store the injected services
    public function __construct(EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher, AccountProxy $current_user) {
        $this->entityTypeManager = $entity_type_manager;
        $this->eventDispatcher = $event_dispatcher;
        $this->currentUser = $current_user;
      }
  2. Implement create() method and use the service container to pass the services needed by the constructor:
    public static function create(ContainerInterface $container) {
        return new static(
          $container->get('entity_type.manager'),
          $container->get('event_dispatcher'),
          $container->get('current_user')
        );
      }

Plugins

The Plugin system in Drupal is a design pattern. Plugins are used to solve problems where we need to provide the user with a specific functionality while allowing them to perform some variations on it. Usually, a user interface enables the website administrators to choose the options they want and configure them according to their needs.

There are many examples of plugin types in Drupal: blocks, field types, widgets, formatters, text filters, views, migration sources, entity types… all of them working as functionalities for specific use cases. Drupal provides a template for them, so we can create multiple instances of each one and configure their different variations according to our needs.

We can say that a plugin in Drupal is one piece of functionality or one instance of the plugin type. The Plugin API has three main components:

  • Plugin Type - This isn’t something we have in code. It’s a concept, so a plugin type is a term that covers plugins sharing the same features and also describes their purpose. For example, “block” is a plugin type, and a block that prints the current user name is an instance of the “block” plugin type. To create a new plugin type, we need to create a plugin manager that “orchestrates” the types.
  • Plugin Discovery - It’s the process of finding plugins for a specific plugin type use case.
  • Plugin Factory - It’s what instantiates the plugin chosen for a particular case.

The last two are defined in code by what we call a Plugin Manager. This is the central class that defines how the plugins are discovered (defining a discovery method) and instantiated (adding a factory). Normally, the Plugin Manager is what we call when we need to invoke a plugin type. For example, the Plugin Manager for blocks is called BlockManager: it extends the DefaultPluginManager, and its constructor comprises the definition of how the discovery and instantiation happen. It goes like this:

public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
    parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\\Core\\Block\\BlockPluginInterface', 'Drupal\\Core\\Block\\Annotation\\Block');
    $this
      ->alterInfo('block');
    $this
      ->setCacheBackend($cache_backend, 'block_plugins');
  }

As you can see, when it calls parent::__construct the 4th parameter is the BlogPluginInterface which is used to define the base template for all the instances created by the factory and the 5th parameter is a class that defines the annotation needed for this plugin type so that Drupal can do the plugin discovery.

A crucial concept when working with plugins is annotation. Basically, annotations are structured comments which give information about a class, and we use them for registering plugins and describing their metadata. The annotation syntax comes from Doctrine and their structure is of type key => value, where the values can be strings, numbers, booleans, lists or more associative data structures (they support nesting).

For example, to create a new block plugin to print the user name, we need to create a class for it, and as we're talking about block plugins, we must use BlockManager as a Plugin Manager. As part of this plugin type, Drupal also provides a base class as an example of how to create an instance of this type, the BlockBase. So in order to create a custom block plugin, we can create a new class that extends the BlockBase class with its corresponding annotation, so that Drupal can discover it. It goes like this:

<?php
namespace Drupal\hello_world\Plugin\Block;
use Drupal\Core\Block\BlockBase;

/**
 * Provides a 'Hello' Block.
 *
 * @Block(
 *   id = "hello_block",
 *   admin_label = @Translation("Hello block"),
 *   category = @Translation("Hello World"),
 * )
 */
class HelloBlock extends BlockBase

Events

When we have complex systems and keep adding new components to them, the components must have a way to communicate with each other. In Drupal, this happens via Events. They are part of the Symfony framework and allow different parts of the system to interact with each other.

Instead of having different objects referring to each other directly, we have a mediator object in charge of facilitating this communication. Here we’re dealing with different parts:

  • One component in the system can dispatch an event on specific conditions;
  • Multiple components in the system can subscribe to that event;
  • Whenever an event is triggered (or dispatched), the mediator object lets the subscribers know about it so they can execute their custom logic.

We need to create events when critical actions happen in our code and we want to give other components the option of reacting to it. To create an event, we must:

  1. Define the name and document it - It’s good practice to define a static class with constants for each event name and document there what the event is. For example, in the Drupal Examples module, we have a class called IncidentEvents with a constant called NEW_REPORT which value is "events_example.new_incident_report". We should note that both the event subscriber and the event dispatcher can refer to the event "events_example.new_incident_report" via the static variable NEW_REPORT. 
  2. Create the event object - This is the actual object that will be instantiated when the event is dispatched. In order to do this, we must create another class that should extend the Event class from the Symfony EventDispatcher component (Symfony\Component\EventDispatcher\Event). This class should have everything necessary for passing over all the contextual data needed for the event. Following the same example as before, we'll see that in the events_example module,

Dispatching an event

In order to dispatch an event, we need to call the dispatch() method from the event_subscriber service. This method receives two arguments: the name of the event being dispatched and the instance of the object that will be passed to every subscriber.

We have an example of this in the same events_example module discussed above, in which an event is dispatched every time a user fills out an incident report form, so they trigger the events_example.new_incident_report event every time they submit the form. The full form code that dispatches this event can be found in the official git repository. We advise you to pay some extra attention to the submitForm function in charge of the dispatching. This is the related code:

$event = new IncidentReportEvent($type, $report);
$this->eventDispatcher->dispatch(IncidentEvents::NEW_REPORT, $event);

In the first line, there’s an instance of the event object that will be passed to every event subscriber. In the second line, the programmer is dispatching the event by calling the dispatch() method and sending the parameters.

Subscribing to an event

We subscribe to an event when we want to get notified when "something" happens and then react to it to add our own logic or alter how Drupal does a specific thing. To subscribe to an event, we must:

  1. Create service tagged with event_subscriber:
    services:
      download_files.event_subscriber:
        class: Drupal\download_files\EventSubscriber\FileDownloadedEventSubscriber
        arguments: ['@logger.factory', '@entity_type.manager']
        tags:
          - {name: event_subscriber}
  2. Extend Symfony\Component\EventDispatcher\EventSubscriberInterface:
    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    class FileDownloadedEventSubscriber implements EventSubscriberInterface
  3. Implement getSubscribedEvents() to indicate what event we're subscribing to and what‘s the method that will be executed when the event is dispatched.
    public static function getSubscribedEvents() {
        $events[DownloadFilesEvents::FILE_DOWNLOADED][] = ['fileDownloaded'];
        return $events;
    }
  4. Implement the fileDownloaded method with the logic you want to execute when the event is triggered.

The use of plugins, services and events is fundamental in Drupal development, so we want to give you some final recommendations:

  • Use Dependency Injection to manage all the dependencies of your classes.
  • Always create services for your reusable functions
  • Remember that code is the best documentation available, so if you are not sure about how something is done, go check core, there will be useful examples!
  • When working with plugins, whether they are blocks, fields formatters, widgets or anything else, remember to use the base classes! They are incredibly useful.
  • When creating event subscribers: remember to tag them!
Bạn thấy bài viết này như thế nào?
0 reactions

Add new comment

Image CAPTCHA
Enter the characters shown in the image.
Câu nói tâm đắc: “Điều tuyệt với nhất trong cuộc sống là làm được những việc mà người khác tin là không thể!”

Related Articles

Hướng dẫn từng bước set up một project PHP theo mô hình MVC. Bạn có thể sử dụng source này để tiết kiệm thời gian set up cho project của mình.

All examples are example-style, which means they don't follow best practices (e.g. dependency injecton). This is done to keep them as simple as possible.

The rapid evolution of diverse interfaces and applications has given rise to a dizzying array of digital channels to support.

Under Form Validation Define Counter - Character, Countet Maimum - 1000, Counter Maximum Message - %\%d Characters remaining