Skip to main content
Drupal

As one of the most popular open-source content management systems (CMS), Drupal provides a powerful platform for building complex web applications. To ensure that Drupal projects are scalable, maintainable, and extensible, it is essential to follow best practices for writing modular and clean code. One such set of principles that can greatly aid in achieving these goals is SOLID principles. SOLID is an acronym that represents a set of five principles - Single Responsibility Principle (SRP), Open/Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), and Dependency Inversion Principle (DIP). In this article, we will explore how SOLID principles are applied in Drupal with examples and any code you write should also follow these SOLID principles.

Single Responsibility Principle (SRP):

The SRP states that a class or module should have only one reason to change, meaning it should have a single responsibility. In Drupal, this can be achieved by ensuring that each module, plugin, or class has a clear and well-defined purpose.

Let's consider an example of a class in a custom Drupal module that handles user authentication and user account creation. Without adhering to the Single Responsibility Principle (SRP), the class might look like this:
 

/**
 * Custom module for user authentication and account creation.
 */
class CustomAuthModule {

  // Authenticate user.
  public function authenticateUser($username, $password) {
    // Code for user authentication.
  }

  // Create user account.
  public function createUserAccount($username, $password, $email) {
    // Code for creating a user account.
  }

  // Other functionalities related to user management.
  public function changePassword($username, $oldPassword, $newPassword) {
    // Code for changing password.
  }

  public function deleteUserAccount($username) {
    // Code for deleting a user account.
  }

  // ... Other methods related to user management.
}

In this example, the CustomAuthModule class is responsible for both user authentication and user account creation, as well as other functionalities related to user management, such as changing passwords and deleting accounts. However, this violates the SRP, as the class has multiple responsibilities and may need to be modified for various reasons, such as changes in user authentication logic, user account creation logic, or user management logic.

To adhere to the SRP, we can refactor the module by separating the responsibilities into separate classes. For example:

/**
 * Custom module for user authentication.
 */
class CustomAuthModule {

  // Authenticate user.
  public function authenticateUser($username, $password) {
    // Code for user authentication.
  }
}

/**
 * Custom module for user account creation.
 */
class CustomAccountModule {

  // Create user account.
  public function createUserAccount($username, $password, $email) {
    // Code for creating a user account.
  }

  // Other functionalities related to user management.
  public function changePassword($username, $oldPassword, $newPassword) {
    // Code for changing password.
  }

  public function deleteUserAccount($username) {
    // Code for deleting a user account.
  }

  // ... Other methods related to user management.
}

In this refactored example, the CustomAuthModule class is responsible only for user authentication, while the CustomAccountModule class is responsible for user account creation and other functionalities related to user management. This separation of responsibilities makes the code more modular and easier to understand, test, and maintain. For instance, if there is a need to change the logic of user authentication, it can be done in the CustomAuthModule class without affecting the CustomAccountModule class or other parts of the system that depend on user account creation or user management functionalities.

Every module and service in Drupal is built with this in mind. The NodeStorage class does only one thing, manages the storage of the node; i.e., save, delete. When you extend FormBase, you are given buildForm, validateForm and submitForm so that you keep your build, validation and submission logic separate. There are plenty more examples of this in Drupal. 

Open/Closed Principle (OCP):

The OCP states that a class or module should be open for extension but closed for modification, meaning that it should be easy to extend its behavior without modifying its existing code. In Drupal, this can be achieved by leveraging Drupal's plugin system or by using hooks and events.

In Drupal, the node entity is a core entity type that represents content of type "node", such as articles, pages, or custom content types. Let's take a simple example of how the node entity follows the Open/Closed Principle (OCP) by being open for extension but closed for modification. 

/**
 * Custom module implementing hook_entity_type_alter().
 */
function my_module_entity_type_alter(array &$entity_types) {
  $entity_types['node']
    ->setStorageClass('Drupal\\my_module\\MyCustomNodeStorage');
}

In this example, we have a custom module called "my_module" that implements the hook_entity_type_alter() hook. This hook is provided by Drupal core and allows modules to alter the definition of entity types.

In the my_module_entity_type_alter() function, we are changing the storage class of the node entity type.

By using the hook_entity_type_alter() hook, we are extending the behavior of the node entity without modifying its code directly. This follows the OCP, as the node entity is closed for modification (we are not directly modifying the node entity code) and open for extension (we are creating a new class which extends the existing NodeStorage class and using that).

This approach allows for customizing the behavior of the node entity in a modular and extensible way. Other modules or custom code can also implement the hook_entity_type_alter() hook to further extend the behavior of the node entity or other entity types, without modifying their code directly. This promotes code reusability, maintainability, and separation of concerns, which are key principles of good software design.

If we wish to alter a base field (it's class), we can do so by using the hook_entity_base_field_info_alter() hook, but we cannot change the base field directly. Sample code from drupal.org: 

function hook_entity_base_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type) {

  // Alter the mymodule_text field to use a custom class.
  if ($entity_type
    ->id() == 'node' && !empty($fields['mymodule_text'])) {
    $fields['mymodule_text']
      ->setClass('\\Drupal\\anothermodule\\EntityComputedText');
  }
}

In the above code sample, we are overriding the class for the field mymodule_text and extending it similar to what we did with NodeStorage class above. We cannot modify code in the existing class, but we can override and extend using a new class.

Liskov Substitution Principle (LSP):

The LSP states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. In Drupal, this means that when extending classes or implementing interfaces, the child class should not break the expected behavior of the parent class or interface.

Let's take a look at an example of how the Liskov Substitution Principle can be applied in Drupal using both the hook_ENTITY_TYPE_update() hook and the hook_entity_update() hook.

/**
 * Implements hook_ENTITY_TYPE_update().
 */
function mymodule_node_update(NodeInterface $node) {
  // Perform actions when a node of any type is updated.
  // ...
}

/**
 * Implements hook_entity_update().
 */
function mymodule_entity_update(EntityInterface $entity) {
  if ($entity instanceof NodeInterface) {
    // Perform actions when any node type is updated.
    // ...
  }
}

In this example, we have implemented both the hook_ENTITY_TYPE_update() and hook_entity_update() hooks. The hook_ENTITY_TYPE_update() hook is specific to nodes, where ENTITY_TYPE is replaced with node, and it takes a single parameter, $node, which is an object implementing the NodeInterface interface. The hook_entity_update() hook, on the other hand, takes a generic parameter $entity, which is an object implementing the EntityInterface interface, which is the parent interface of NodeInterface.

According to the Liskov Substitution Principle, objects of a subclass should be able to replace objects of the superclass without altering the desirable properties of the program. In this case, the NodeInterface is a more specific interface that extends the EntityInterface, which is a more general interface for all entities in Drupal.

This allows us to use the NodeInterface in the hook_entity_update() hook, which is a more general hook that can be used for any entity type in Drupal. We can also use the EntityInterface in the hook_ENTITY_TYPE_update() hook, which is a specific hook for nodes. This allows for interchangeability of different entity types without altering the behavior of the hooks, following the Liskov Substitution Principle.

For example, if we have a custom entity type called my_entity that implements the EntityInterface, we can also use it in the hook_entity_update() function without altering the behavior of the hook:

/**
 * Implements hook_entity_update().
 */
function mymodule_entity_update(EntityInterface $entity) {
  if ($entity instanceof NodeInterface) {
    // Perform actions when any node type is updated.
    // ...
  }

  if ($entity instanceof MyEntityInterface) {
    // Perform actions specific to custom entity type.
    // ...
  }
}

In this example, we can see that we are able to use the EntityInterface as the type hint for the $entity parameter in the hook_entity_update() hook, which allows for interchangeability of different entity types without altering the behavior of the hook. This demonstrates how Drupal follows the Liskov Substitution Principle, as we can substitute different entity types without altering the behavior of the hooks, allowing for flexibility and extensibility when working with entities in Drupal.

This also means that in hook_node_update() hook, we can change the type hint of the entity argument from NodeInterface to EntityInterface and the code should work just the same:
 

/**
 * Implements hook_ENTITY_TYPE_update().
 */
function mymodule_node_update(EntityInterface $node) {
  // Perform actions when a node of any type is updated.
  // ...
}

Interface Segregation Principle (ISP):

The ISP states that clients should not be forced to depend on interfaces they do not use. In Drupal, this means that interfaces should be kept focused and specific, providing only the necessary methods for the consumers that implement them.

Let's take a look at an example of how the Interface Segregation Principle can be applied in Drupal using the interfaces used by the node entity.

In Drupal, the NodeInterface is a core interface that represents a node entity, and it extends several other interfaces, such as EntityInterface, RevisionableInterface, and FieldableEntityInterface. Each of these interfaces represents a specific set of functionality for entities in Drupal. Let's consider an example where we have a custom module that needs to implement only a specific subset of functionality for nodes, and we want to adhere to the Interface Segregation Principle by only implementing the necessary interfaces.

use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\RevisionLogEntityTrait;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\node\NodeInterface;
use Drupal\node\NodeStorageInterface;
use Drupal\node\NodeInterface;

/**
 * @ContentEntityType(
 *   id = "my_module_custom_node",
 *   label = @Translation("Custom Node"),
 *   handlers = {
 *     "storage" = "Drupal\node\NodeStorageInterface",
 *     "access" = "Drupal\node\NodeAccessControlHandler",
 *     "views_data" = "Drupal\node\NodeViewsData",
 *     "form" = {
 *       "default" = "Drupal\node\NodeForm",
 *     },
 *     "route_provider" = {
 *       "html" = "Drupal\node\NodeHtmlRouteProvider",
 *     },
 *   },
 *   ...
 * )
 */
class MyModuleCustomNode extends ContentEntityBase implements NodeInterface, RevisionableInterface {
  use EntityChangedTrait;
  use RevisionLogEntityTrait;

  // Implement custom functionality specific to this entity type.
}

In this example, we are implementing a custom node entity called MyModuleCustomNode that extends ContentEntityBase, a base class provided by Drupal core for content entities. The MyModuleCustomNode class implements the NodeInterface and RevisionableInterface interfaces, which represent the functionality needed for nodes and revisionable entities respectively.

By implementing only the necessary interfaces, we are adhering to the Interface Segregation Principle, which states that clients should not be forced to depend on interfaces they don't use. This allows us to create entities with specific functionality without being burdened with unnecessary interfaces, making the code more maintainable and efficient.

For example, if we don't need revisionable functionality for our custom node entity, we can simply omit the RevisionableInterface and its related traits from our implementation. Similarly, if we don't need fieldable functionality, we can omit the FieldableEntityInterface and its related traits. This flexibility allows us to create custom entities that only implement the interfaces and functionality needed for our specific use case, following the Interface Segregation Principle in Drupal.

If you look at the Node.php (Node entity class) file and follow the classes that Node class extends, you'll see a lot more interfaces like EntityChangedInterface (entity change timestamp tracking), EntityPublishedInterface (track entity's published state), EntityOwnerInterface (entities that have an owner which is a user entity reference). So if you create a custom entity and if it is not required for the entity to have a created user, then there is no need to extend (inherit) the EntityOwnerInterface interface.

Dependency Inversion Principle (DIP):

The DIP states that high-level modules or classes should not depend on low-level modules or classes, but both should depend on abstractions. In Drupal, this means that classes or modules should depend on interfaces or abstract classes rather than concrete implementations.

In Drupal, the Dependency Injection Container is a powerful tool that allows us to implement the Dependency Inversion Principle. The Dependency Injection Container is responsible for managing and injecting dependencies into services, plugins, and other objects in a Drupal application.

Let's consider an example where we have a custom module that needs to send emails. Instead of directly instantiating and using the Drupal\Core\Mail\MailManagerInterface service, which represents the built-in Drupal mail manager, we can use the Dependency Injection Container to invert the dependency and inject the mail manager into our custom class.

use Drupal\Core\Mail\MailManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Example custom class that sends emails.
 */
class MyCustomEmailSender implements ContainerFactoryPluginInterface {

  /**
   * The Mail Manager service.
   *
   * @var \Drupal\Core\Mail\MailManagerInterface
   */
  protected $mailManager;

  /**
   * Constructs a new MyCustomEmailSender object.
   *
   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
   *   The Mail Manager service.
   */
  public function __construct(MailManagerInterface $mail_manager) {
    $this->mailManager = $mail_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    // Use the Drupal core service container to instantiate the Mail Manager.
    $mail_manager = $container->get('plugin.manager.mail');
    return new static($mail_manager);
  }

  /**
   * Sends an email.
   *
   * @param string $to
   *   The email recipient.
   * @param string $subject
   *   The email subject.
   * @param string $body
   *   The email body.
   */
  public function sendEmail($to, $subject, $body) {
    // Use the injected Mail Manager to send the email.
    $this->mailManager->mail('my_module', 'my_mail_key', $to, 'en', [
      'subject' => $subject,
      'body' => $body,
    ]);
  }

}

In this example, we have a custom class called MyCustomEmailSender that sends emails. Instead of directly instantiating the MailManagerInterface service, we inject it into our class through the constructor using type hinting. We also implement the ContainerFactoryPluginInterface interface, which allows us to use the create() method to instantiate the class using the Drupal core service container.

By using the Dependency Injection Container and injecting the MailManagerInterface into our custom class, we are adhering to the Dependency Inversion Principle. Our custom class is not directly dependent on concrete implementations, but rather on interfaces or abstract classes, which makes our code more flexible, extensible, and easier to test.

This is also means that when we enable a module like smtp, we don't need to make any changes to the above code and the $this->mailManager will automatically use the mail manager defined by the smtp module. 

Another example of how the Dependency Inversion Principle (DIP) can be applied in Drupal.

In Drupal, the Dependency Injection Container is a core component that allows for the inversion of dependencies in a modular way, adhering to the DIP. The container is responsible for managing and injecting dependencies into services, plugins, and other components in a decoupled manner.

Here's a simple example of DIP in Drupal, where we have a custom module that needs to access the database service provided by Drupal core:

use Drupal\Core\Database\Connection;

/**
 * Custom service that uses the database service.
 */
class MyCustomService {

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * MyCustomService constructor.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection.
   */
  public function __construct(Connection $database) {
    $this->database = $database;
  }

  /**
   * Custom method that uses the database service.
   */
  public function doSomethingWithDatabase() {
    // Use the database service.
    $query = $this->database->select('mytable', 'm')
      ->fields('m', ['id', 'name'])
      ->condition('status', 1)
      ->execute();
    // Process the query result.
    // ...
  }
}

In this example, we have a custom service called MyCustomService that needs to use the database service provided by Drupal core. Instead of instantiating the database service directly, we use the DIP by injecting the Connection object into the constructor of MyCustomService.

By injecting the database service as a dependency, we are adhering to the Dependency Inversion Principle, which states that high-level modules or components should not depend on low-level modules or components directly, but rather on abstractions or interfaces. In this case, MyCustomService depends on the Connection abstraction provided by Drupal core, which allows for flexibility in choosing different database implementations (mysql, pgsql core modules) or swapping out the database service with a mock object (see StubConnection.php) for testing purposes, without changing the code of MyCustomService.

This decoupling of dependencies through the use of a dependency injection container in Drupal allows for more maintainable, testable, and extensible code, following the principles of DIP.

x

Work

Therefore logo
80 Atlantic Ave, Toronto, ON Canada
Email: hello@therefore.ca
Call us: +1 4166405376
Linkedin

Let us know how we can help!