Symfonic Drupal principles

This is my notes about the general principles behind Drupal OOP (Object Oriented Programming that should be used for Symfonic Drupal.

Table of contents

Introduction

The Symfony framework provides a set of decoupled and reusable PHP libraries. Since these libraries are reused in thousands of projects, they provide a tested and solid code-base for PHP applications.

Some links to useful pages about new concepts in Symfonic Drupal:

Blogs:

Training:

Architecture

Inversion of Control

Design principles are high level guidelines for design. Patterns implements principles.

Inversion of Control (IoC)Principle
Dependency Injection (DI)Pattern

As illustrated in the above figure, IoC is a high level design principle which should be used while designing application classes. They recommend certain best practices, but do not provide any specific implementation details. Dependency Injection (DI) is a pattern that provides implementation details.

IoC is used to decouple dependencies between high-level and low-level layers through shared abstractions.

To illustrate IoC, first observe that in procedural programming, software components are designed to operate on and control the execution environment. A logging module may write events into a log file. If we want to log to a database, or to send emails about logged events, this functionality to do this can be added to the module. Doing so works, but bloats the module as functions for more and more environments are added.

Inversion of Control (IoC) is a principle that inverts this. Instead of making the module responsible for logging data to multiple endpoints, this responsibility is delegated to the external environments. The logging module's implementation would remain simple, limited to acting as a simple event dispatcher. The environment components would act upon these events and implement all of the logic required to log the data to a file, to the database, to send email, etc.

Inversion of control encourages Single-responsibility principle (discussed in the next section).

In symfonic Drupal, a service is a PHP class with some code that provides a single specific functionality that can be used throughout the website or application. You can easily access each service and use its functionality wherever you need it. This is called service-oriented architecture which is not unique to Drupal, Symfony or even PHP.

In symfonic Drupal, a service is any object managed by the Symfony service container.

The concept of services allows Drupal developers to decouple reusable functionality and makes these services pluggable and replaceable by registering them with a service container. As a developer, the best practice is to access any of the services provided by Drupal via the service container to ensure the decoupled nature of these systems is respected.

As a developer, services are used to perform operations like accessing the database or sending an email. Rather than use PHP's native MySQL functions, we use the core-provided service via the service container to perform this operation so that our code can simply access the database without having to worry about whether the database is MySQL or SQLlite, or if the mechanism for sending email is SMTP or something else.

Symfonic Drupal contains a large number of services. To see an introduction, read DO: Structure of a service file. It suggests that more examples can be found in core.services.yml or any module's service file. If the class is not a service class, there is no entry in the .services.yml. You may also get a list of those that are available is by looking at the CoreServiceProvider.php and core.services.yml files.

Drush will provide a list of all Drupal container services:

$ drush dcs
$ drush dcs date

The first list lists all Drupal container services. The second all those containing the string "date".

Dependency Injection (DI) is a design pattern following the IOC principle. It is the preferred method for accessing and using services in symfonic Drupal. Rather than calling the global services container (\Drupal), services are instead passed as arguments to a class constructor. Many of the controller and plugin classes provided by modules in core make use of this pattern and serve as a good resource for examples on how to do it.

The chapter about upgrading extensions to the next major version of symfonic Drupal contains a section discussing how to upgrade static call to use Dependency injection.

Example 1: Config setting

Using a \Drupal call (deprecated):

\Drupal::config('my_custom_module.settings'):

Injected:

$this->config->get('my_custom_module.settings');

Note that to be able to do this, you need to inject the config.factory service to able to do this. For the full story, read DSE: Check config setting.

Example 2: Load node data

Using the static function of a class to load node data (deprecated):

use Drupal\node\Entity\Node;

public function content_load($node = NULL) {
 $node = Node::load($node, NULL, TRUE);
}

Using dependecy injection to load node data.

use Drupal\Core\Entity\EntityTypeManagerInterface;
…
  $node_storage = $this->entityTypeManager()->getStorage('node');
  $node = $node_storage->load($nid);
…

Source DSE: Dependency injection to load node data. See the reply by leymannx for an expanded description.

DSE: How to use dependency injection to load node data? JayPan

Single-responsibility principle

The single-responsibility principle (SRP) is a computer-programming principle first formulated by Robert C. Martin. It can be expressed as:

A class should have one, and only one, reason to change.

Basically, the principle says that a class (or some other software component) should have a single responsibility. If it has more, it becomes more difficult to manage. It is closely related to separation of concerns.

Stackify - SOLID Design Principles Explained: The Single Responsibility Principle

Plugins

DO: Plugin API overview

DO: Annotations-based plugins

[TBA]

OO vs procedural context

As noted above, the preferred design pattern in Drupal is DI. For instance, to get the path of the temporary directory, use:

$tmp = $this->fileSystem->getTempDirectory();

This is not meaningful in a procedural context. Instead, call the static service container to obtain an object, and then use the correct method of that object to get the result.

To find the name of the core service to call examine core.services.yml. For instance, we see that it is "file_system":

services:
  …
  file_system:
    class: Drupal\Core\File\FileSystem
    arguments: ['@stream_wrapper_manager', '@settings', '@logger.channel.file']

Resulting in this code:

$service = Drupal::service('file_system');
$tmp = $service->getTempDirectory();

Sources

Configuration settings

Some configuration still go into settings.php, but the $conf[] array has been replaced by the $settings[] array. Examples:

$settings['file_public_path'] = 'sites/default/files';
$settings['file_private_path'] = '';

The path to public and private files can no longer be set in the GUI, but must be set in settings.php.

Entity API

The Entity API ptovides methods for CRUD manipulation of entities.

Entities are instances of typed classes with methods. Many of these methos allow inpection of fields. Examples:

Generic methods:

Use getString() if you expect the field to contain a single value. Use getValue() if the field may contain more than one value.

Type specific methods:

A protected value is only accessible from the class itself. To access it from outside the class use of the public methods provided by the class. Examples:

$title = $entity->getTitle();
$body = $entity->getBody();
$myfield = $entity->field_myfield->getString();
$myarray = $entity->field_myfield->getValue();

Some protected objects can be accessed by casting them to a string. Here $variables['page']['#title'] is an object of type "TranslatableMarkup", and this gives access to the title:

$title = (string)$variables['page']['#title'];

Source: DSE - How to access the value of a Drupal\Core\StringTranslation\TranslatableMarkup object within a form?.

In a content entity the protected $values array contains the original values of the entity. The actual field values are stored in the protected array $fields, which holds an array of FieldItemList objects. These are lazily built, so when the node is loaded this array is empty. When you get/set fields through the public entity methods and properties the field objects with the actual data will be created and stored in this internal array.

For debugging purpose you can force the $fields array to be fully populated by:

$values = $entity->toArray();
https://gorannikolovski.com/blog/various-ways-updating-field-values-drupal-8-and-9

Strings in markup objects

The string in markup object is protected. This is how it looks like when rendered by debug():

$markup => stdClass Object
(
    [__CLASS__] => Drupal\Core\Render\Markup
    [string:protected] => 'Some string'.
)

Looking at the Markup class, the public methods are:

technicalMagic methods are special methods which override PHP's default's action when certain actions are performed on an object. The magic method __toString() lets you get the string contained in the instance by using the instance as a string.

$text = (string)$markup;

Managing projects with composer

In symfonic Drupal, project's that rely on external libraries should have a file named composer.json in the module's root directory. This file should, among other things declare dependencies upon external libraries with the keyword require. This allows composer to manage the project's dependencies.

However, to make composer do this automatically, you must download the core using composer, and you must use composer to install Drupal modules.

Final word

[TBA]


Last update: 2021-03-23 [gh].