Skip to main content
Patterns

Drupal follows best practices in software development by utilizing various design patterns. Design patterns are established solutions for common programming challenges, providing a structured and reusable approach to software design. In Drupal, these design patterns are used to ensure extensibility, flexibility, scalability and maintainability of the CMS. From the Observer pattern for event handling, to the Plugin pattern for modular functionality, to the Factory pattern for object creation, Drupal leverages a variety of design patterns to implement its robust architecture. This introduction provides a glimpse into the key design patterns used in Drupal CMS, which play a crucial role in shaping its modular and flexible nature.

  1. Observer Pattern: Drupal uses the Observer pattern to implement its event system, which allows modules to react to events and perform actions accordingly. This pattern decouples the code, allowing for extensibility and flexibility.

    Here's an example of how Drupal uses the Observer pattern:

    In Drupal, when a user creates a new node (content item), an event called "node_insert" is triggered. Modules can subscribe to this event and register event listeners, which are callback functions that get executed when the event is fired. These event listeners can perform additional actions, such as sending notifications, updating other entities, or performing custom logic.

    For example, let's say you have a module called "MyModule" in Drupal that needs to perform some actions whenever a new node is created. You can implement the Observer pattern in Drupal by defining an event listener function in your module, like this:
     

    use Drupal\node\NodeInterface;
    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    
    class MyModuleEventSubscriber implements EventSubscriberInterface {
    
      /**
       * {@inheritdoc}
       */
      public static function getSubscribedEvents() {
        // Define the event and the callback function to handle it.
        $events['node_insert'][] = ['onNodeInsert'];
        return $events;
      }
    
      /**
       * Event callback function for node_insert event.
       */
      public function onNodeInsert(NodeInterface $node) {
        // Perform custom actions when a new node is created.
        // ...
      }
    }
    

    In this example, the getSubscribedEvents() method defines that the onNodeInsert() function should be called when the "node_insert" event is fired. The onNodeInsert() function is the event listener that gets executed when a new node is created. It can access the $node object, which represents the newly created node, and perform custom actions accordingly.

    By using the Observer pattern, Drupal allows modules like "MyModule" to react to events and perform actions in a decoupled manner, enhancing extensibility and flexibility in the CMS. Other modules can also subscribe to the same event or define their own events, allowing for a modular and flexible system where different parts of Drupal can react to events and respond accordingly. 

    Here's a link to list of available Drupal core events: https://api.drupal.org/api/drupal/core%21core.api.php/group/events/10

  2. Plugin Pattern: Drupal uses the Plugin pattern to allow modules to define plugin types and provide plugin implementations for different functionality. Plugins in Drupal can be used for various purposes such as blocks, fields, form elements, and more.

    Drupal makes extensive use of the Plugin design pattern to provide modular functionality that can be extended and customized by different modules. Here's an example of how Drupal uses the Plugin pattern:

    In Drupal, the Block system allows users to create and manage blocks of content that can be displayed in various regions of a website. Blocks in Drupal are implemented as plugins, and different modules can provide their own block plugins to add custom functionality.

    For example, let's say you have a module called "MyModule" in Drupal that needs to add a custom block with specific functionality. You can implement the Plugin pattern in Drupal by defining a block plugin in your module, like this:
     

    use Drupal\Core\Block\BlockBase;
    use Drupal\Core\Form\FormStateInterface;
    
    /**
     * Provides a custom block for MyModule.
     *
     * @Block(
     *   id = "mymodule_custom_block",
     *   admin_label = @Translation("MyModule Custom Block"),
     * )
     */
    class MyModuleCustomBlock extends BlockBase {
    
      /**
       * {@inheritdoc}
       */
      public function build() {
        // Build the block content.
        $output = '';
        // ...
        return [
          '#markup' => $output,
        ];
      }
    
      /**
       * {@inheritdoc}
       */
      public function blockForm($form, FormStateInterface $form_state) {
        // Implement block configuration form.
        // ...
        return $form;
      }
    
      /**
       * {@inheritdoc}
       */
      public function blockSubmit($form, FormStateInterface $form_state) {
        // Process block configuration form submission.
        // ...
      }
    }
    

    In this example, the MyModuleCustomBlock class extends the BlockBase class, which is a base class for block plugins in Drupal. The @Block annotation defines the plugin ID and other metadata for the block plugin, such as the administrative label.

    The build() method defines how the block content is rendered, the blockForm() method defines the configuration form for the block, and the blockSubmit() method handles the submission of the configuration form. These methods are part of the Plugin interface that is used by Drupal to define plugin functionality.

    By using the Plugin pattern, Drupal allows modules like "MyModule" to provide custom block plugins that can be managed and rendered through the Block system. Other modules or even the site administrator can enable, configure, and use these block plugins in various regions of the website, providing a modular and extensible way to add custom functionality to Drupal.

    Plugins in Drupal are very versatile. Plugins can have various properties and features. There a plugins that have no UI component attached to it like mail managers, cache bins, etc; And then there are plugins that are tightly coupled with UI components in some way like field widgets, formatters, types, view fields, and so on. Here's Drupal's official documentation on plugins: https://www.drupal.org/docs/drupal-apis/plugin-api

  3. Factory Pattern: Drupal uses the Factory pattern loosely to create objects in a flexible and configurable way. Drupal's dependency injection container, known as the Service Container, is an example of a factory pattern that provides services to different parts of the system.

    Drupal does not extensively use the Factory design pattern as a standalone pattern in its core architecture. However, Drupal does use factory-like concepts and techniques in some parts of its codebase, but they are not strictly adhering to the formal Factory design pattern.

    In Drupal, factory-like concepts can be found in the form of service containers and dependency injection (DI) container. Drupal's service container is responsible for managing and instantiating objects, which can be considered as a form of factory pattern. Here's an example:

    In Drupal, the service container is responsible for managing and creating objects for different services, such as database connections, caching, and logging. These services are defined as service definitions in Drupal's configuration files, and the service container instantiates these services as objects when they are requested by other parts of the system.

    Drupal's config.factory service is an example of how Drupal uses a factory-like pattern to manage and create configuration objects. The config.factory service allows modules and themes to create instances of configuration objects defined in Drupal's configuration system. Here's an example:
     

    use Drupal\Core\Config\ConfigFactoryInterface;
    
    class MyModuleConfigManager {
    
      protected $configFactory;
    
      public function __construct(ConfigFactoryInterface $configFactory) {
        $this->configFactory = $configFactory;
      }
    
      public function getConfig($configName) {
        // Create an instance of the configuration object.
        $config = $this->configFactory->get($configName);
        return $config->get();
      }
    
    }
    

    In this example, the MyModuleConfigManager class has a dependency on the ConfigFactoryInterface service, which is a factory-like service provided by Drupal's core service container. The ConfigFactoryInterface service allows the MyModuleConfigManager class to create instances of configuration objects defined in Drupal's configuration system by calling the get() method with the configuration name as an argument.

    The configuration objects in Drupal represent different configuration items, such as site settings, module settings, and theme settings, and are stored in the Drupal configuration system, which is based on YAML files. The config.factory service provides a way to create and manage these configuration objects, allowing modules and themes to easily access and modify configuration data.

    By using the config.factory service, Drupal follows a factory-like pattern to manage and create configuration objects, providing a flexible and extensible way to handle configuration data in a modular and organized manner.

  4. Adapter Pattern: Drupal uses the Adapter pattern to allow modules to interact with external systems or APIs in a consistent and unified way. Adapters in Drupal are used to provide a common interface for different external systems, making it easier to switch between them. Here's an example:

    One common use case of the Adapter pattern in Drupal is when integrating with external libraries or APIs that have a different interface than what Drupal expects. For instance, Drupal may need to interact with an external service that has its own unique API, but Drupal's codebase is designed to work with a standard interface or API. In such cases, an Adapter can be used to translate or adapt the external API to the standard Drupal API.

    Let's take an example of a module called "MyModule" that needs to integrate with an external authentication service that has a different API than Drupal's standard authentication system. In this case, an Adapter can be used to adapt the external authentication service's API to Drupal's standard authentication system:
     

    use Drupal\Core\Session\AccountInterface;
    use Drupal\MyModule\ExternalAuth\ExternalAuthServiceInterface;
    
    class MyModuleAuthAdapter implements AccountInterface {
    
      protected $externalAuthService;
    
      public function __construct(ExternalAuthServiceInterface $externalAuthService) {
        $this->externalAuthService = $externalAuthService;
      }
    
      public function id() {
        // Adapt the external service's user ID to Drupal's user ID.
        return $this->externalAuthService->getUserId();
      }
    
      public function isAuthenticated() {
        // Adapt the external service's authentication status to Drupal's authentication status.
        return $this->externalAuthService->isAuthenticated();
      }
    
      // ... other methods that Drupal's AccountInterface requires
    
    }
    

    In this example, the MyModuleAuthAdapter class adapts the interface of an external authentication service, represented by the ExternalAuthServiceInterface, to Drupal's AccountInterface. The MyModuleAuthAdapter class implements the methods required by Drupal's AccountInterface and internally uses the methods of the ExternalAuthServiceInterface to adapt the external service's API to Drupal's standard authentication system.

    By using the Adapter pattern, Drupal can seamlessly integrate with external libraries or services that have different interfaces, without having to modify or tightly couple its core codebase to those interfaces. The Adapter pattern allows for flexibility and decoupling, making it easier to maintain and extend Drupal's functionality.

    Another example of this is the Drupal contrib module s3fs module which can completely replace the default public file system with AWS S3. 

  5. Decorator Pattern: Drupal uses the Decorator pattern to modify or extend the behavior of objects dynamically. In Drupal, decorators are used to add functionality to entities, forms, and other objects without changing their original implementation at runtime; One of the reasons why decorator pattern can be applied to only public methods. Here's an example:

    In Drupal, the Decorator pattern is often used in the context of altering the rendering of content, such as nodes, blocks, or fields. Drupal's render system uses render arrays to define the output of content, and decorators can be used to add or modify the rendering behavior of these render arrays.

    Let's take an example of a module called "MyModule" that needs to add additional markup to the rendered output of a node's body field. In this case, a Decorator can be used to add the extra markup to the rendering of the body field:
     

    use Drupal\Core\Field\FieldItemListInterface;
    use Drupal\Core\Render\Markup;
    
    class MyModuleBodyFieldDecorator implements FieldItemListInterface {
    
      protected $bodyField;
    
      public function __construct(FieldItemListInterface $bodyField) {
        $this->bodyField = $bodyField;
      }
    
      public function view($view_mode = 'default', $langcode = NULL) {
        // Get the original rendering of the body field.
        $output = $this->bodyField->view($view_mode, $langcode);
    
        // Add extra markup to the rendered output.
        $extraMarkup = Markup::create('<div class="extra-markup">Extra markup goes here</div>');
        $output['#markup'] .= $extraMarkup->render();
    
        return $output;
      }
    
      // ... other methods that FieldItemListInterface requires
    
    }
    

    In this example, the MyModuleBodyFieldDecorator class decorates the rendering behavior of a node's body field, which is represented by the FieldItemListInterface. The MyModuleBodyFieldDecorator class implements the view() method required by the FieldItemListInterface and adds extra markup to the rendered output of the body field by modifying the $output array.

    By using the Decorator pattern, Drupal can easily extend or modify the rendering behavior of content without altering the original implementation of the content objects. Decorators provide a flexible way to dynamically modify the behavior of objects at runtime, making it possible to add or remove functionality as needed without having to modify the core codebase.

  6. Singleton Pattern: Drupal uses the Singleton pattern to ensure that certain objects are instantiated only once throughout the system. For example, Drupal's database connection and caching system use the Singleton pattern to provide a single instance of the respective objects.

    Drupal uses the Singleton pattern in various contexts, such as managing global settings, caching, and database connections. Here's an example:

    One common use case of the Singleton pattern in Drupal is in managing global configuration settings that need to be accessed from multiple parts of the codebase. Drupal provides a core service called config.factory that follows the Singleton pattern to provide a centralized way to manage configuration settings.

    Here's an example of how Drupal uses the Singleton pattern with the config.factory service:
     

    use Drupal\Core\Config\ConfigFactoryInterface;
    use Drupal\Core\Config\Config;
    
    class MyModuleConfigManager {
    
      protected $configFactory;
    
      protected $config;
    
      public function __construct(ConfigFactoryInterface $configFactory) {
        $this->configFactory = $configFactory;
      }
    
      public function getConfig($configName) {
        if (!isset($this->config[$configName])) {
          // Load the configuration if it hasn't been loaded yet.
          $this->config[$configName] = $this->configFactory->get($configName);
        }
        return $this->config[$configName];
      }
    
    }
    

    In this example, the MyModuleConfigManager class is a custom class that manages configuration settings for a module. It uses the config.factory service, which is a Singleton, to load and cache configuration objects. The $configFactory object is injected into the class's constructor, and the getConfig() method uses it to retrieve configuration objects. The Singleton pattern ensures that only one instance of the config.factory service is used throughout the request or session, preventing redundant configuration loading and improving performance.

    By using the Singleton pattern, Drupal can ensure that certain objects or services have a single instance during the lifetime of a request or a session, which can help with performance optimization and provide a centralized way to manage global settings or resources. However, it's important to use the Singleton pattern judiciously, as it can introduce global state and potential coupling, and may not be suitable for all use cases.

  7. Chain of Responsibility Pattern: Drupal uses the Chain of Responsibility pattern to handle requests or events through a chain of objects, where each object in the chain has the option to handle the request or pass it on to the next object. Drupal's menu and routing systems are examples of the Chain of Responsibility pattern, where different objects in the chain handle requests based on their configuration.

    One common use case of the Chain of Responsibility pattern in Drupal is in handling HTTP requests during the routing process. Drupal uses a series of middleware handlers in the routing process to handle various tasks such as authentication, caching, and request modification. Each middleware handler in the chain has the option to handle the request or pass it on to the next handler in the chain.

    Here's an example of how Drupal uses the Chain of Responsibility pattern in the routing process:
     

    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    use Symfony\Component\HttpKernel\Event\RequestEvent;
    
    class MyModuleRequestEventSubscriber implements EventSubscriberInterface {
    
      protected $next;
    
      public function __construct(MyModuleRequestEventSubscriber $next = null) {
        $this->next = $next;
      }
    
      public function onKernelRequest(RequestEvent $event) {
        // Check if this subscriber can handle the request event.
        if ($this->canHandle($event)) {
          // Handle the request event.
          $this->processRequest($event);
        }
        else if ($this->next) {
          // Pass the request event to the next subscriber in the chain.
          $this->next->onKernelRequest($event);
        }
      }
    
      protected function canHandle(RequestEvent $event): bool {
        // Check if this subscriber can handle the request event.
        // ... implementation ...
      }
    
      protected function processRequest(RequestEvent $event) {
        // Handle the request event.
        // ... implementation ...
      }
    
      public static function getSubscribedEvents() {
        return [
          // Change the value (1001) to control when you want this to execute.
          // A higher value means it will be earlier. Depending on how early 
          // it is many services may not be available. So put a reasonably high 
          // enough value (say 50 - 100) if you want Drupal to be fully bootstrapped.
          KernelEvents::REQUEST => ['onKernelRequest', 1001],
        ];
      }
    
    }
    

    In this example, the MyModuleRequestEventSubscriber class is an event subscriber that listens to the KernelEvents::REQUEST event, which is triggered during the request processing lifecycle in Drupal. If the subscriber can handle the request event, it processes it using the processRequest() method. Otherwise, it passes the request event to the next subscriber in the chain, which is set during construction using the $next parameter. This allows for a sequential processing of request events by multiple subscribers in the chain until a subscriber can handle the event or the end of the chain is reached.

    By using the Chain of Responsibility pattern in an event subscriber, Drupal can achieve a modular and extensible way to handle request events, allowing for easy addition or removal of subscribers without modifying the core event processing logic. It promotes loose coupling between subscribers and provides a flexible approach to handle various tasks in response to events triggered during the request processing lifecycle.

    Drupal also allows to change the order of the sequence. Say I have an event subscriber in my module and I need it to execute before all other events, I just need to make sure that I set a very high weight to it. In the example above it has a weight of 1001. If you want it to execute last, you can change it to -1001. 

  8. Services and Dependency Injection Pattern: Drupal utilizes the Services and Dependency Injection pattern as a fundamental part of its architecture to provide a flexible and modular system for managing dependencies and implementing inversion of control. In Drupal, services are used to represent reusable objects or components that can be registered and retrieved from a central service container using dependency injection.

    Here's an example of how Drupal utilizes the Services and Dependency Injection pattern:
     

    use Drupal\Core\Config\ConfigFactoryInterface;
    use Drupal\Core\Database\Connection;
    use Drupal\Core\Logger\LoggerChannelFactoryInterface;
    use Drupal\Core\Mail\MailManagerInterface;
    use Drupal\Core\StringTranslation\StringTranslationTrait;
    use Drupal\Core\Url;
    use Symfony\Component\HttpFoundation\RequestStack;
    use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
    
    class MyModuleExampleService {
    
      use StringTranslationTrait;
    
      protected $configFactory;
      protected $connection;
      protected $loggerFactory;
      protected $mailManager;
      protected $requestStack;
      protected $urlGenerator;
    
      public function __construct(ConfigFactoryInterface $configFactory, Connection $connection, LoggerChannelFactoryInterface $loggerFactory, MailManagerInterface $mailManager, RequestStack $requestStack, UrlGeneratorInterface $urlGenerator) {
        $this->configFactory = $configFactory;
        $this->connection = $connection;
        $this->loggerFactory = $loggerFactory;
        $this->mailManager = $mailManager;
        $this->requestStack = $requestStack;
        $this->urlGenerator = $urlGenerator;
      }
    
      public function doSomething() {
        // Use the injected services to perform some operation.
        $config = $this->configFactory->get('my_module.settings');
        $this->connection->insert('my_table')
          ->fields(['field1' => 'value1', 'field2' => 'value2'])
          ->execute();
        $logger = $this->loggerFactory->get('my_module');
        $logger->info('An example log message.');
        $this->mailManager->mail('my_module', 'example_mail_key', 'example@example.com', $this->getLanguage());
        $request = $this->requestStack->getCurrentRequest();
        $url = Url::fromRoute('my_module.route_name')->toString();
        // ...
      }
    
      protected function getLanguage() {
        // Implement the logic to determine the language.
        // ... implementation ...
      }
    
    }
    

    In this example, the MyModuleExampleService class is a custom service defined in a Drupal module. The class has various dependencies, such as ConfigFactoryInterface, Connection, LoggerChannelFactoryInterface, MailManagerInterface, RequestStack, and UrlGeneratorInterface, which are injected into the class's constructor using type-hinting. These injected services are managed by Drupal's dependency injection container, which automatically resolves and provides the appropriate instances when the service is instantiated.

    By using the Services and Dependency Injection pattern, Drupal achieves loose coupling between components, allowing for easier testing, reusability, and maintainability. It also promotes modular development, as services can be easily swapped or extended without modifying the core logic of the service. Additionally, it allows for a centralized and declarative way to manage dependencies and configure services, making it easier to manage complex dependencies and implement inversion of control in Drupal's architecture.

These are some of the design patterns commonly used in Drupal CMS and the ones I have been able to deduce with the help of Chat GPT. Drupal's architecture and module system are designed to follow best practices in software development, including the use of appropriate design patterns for extensibility, flexibility, and maintainability. It is also important to follow these when you are writing code in your custom modules. So, create your interfaces, inject your services and listen to the events. :) 

References

  • https://www.youtube.com/watch?v=WjFj975Bvgo
  • https://chaitanya06.medium.com/identify-the-design-pattern-in-drupal-9bf7ac548d37
  • https://www.specbee.com/blogs/php-design-patterns-in-drupal-9
  • https://www.srijan.net/resources/blog/revisiting-php-design-pattern-concepts-in-drupal-8