logo

Extra Block Types (EBT) - New Layout Builder experienceâť—

Extra Block Types (EBT) - styled, customizable block types: Slideshows, Tabs, Cards, Accordions and many others. Built-in settings for background, DOM Box, javascript plugins. Experience the future of layout building today.

Demo EBT modules Download EBT modules

âť—Extra Paragraph Types (EPT) - New Paragraphs experience

Extra Paragraph Types (EPT) - analogical paragraph based set of modules.

Demo EPT modules Download EPT modules

Scroll

Using PHP Constructor Property Promotion in Drupal Custom Modules

13/06/2025, by Ivan

Menu

PHP 8 introduced constructor property promotion, a feature that simplifies class property definition and assignment by letting you declare and initialize properties in the constructor signature. This tutorial demonstrates how to use constructor property promotion in Drupal custom modules (which require PHP 8.0+), specifically to simplify dependency injection in your services and controllers. We will compare the traditional Drupal pattern (used in PHP 7 and early Drupal 9) with the modern PHP 8+ approach, using full code examples for both. By the end, you’ll see how this modern syntax reduces boilerplate, makes code clearer, and aligns with current best practices.

Drupal 10 (which requires PHP 8.1+) has begun adopting these modern PHP features in core, so custom module developers are encouraged to do the same. Let’s start by reviewing the traditional dependency injection pattern in Drupal, then refactor it using constructor property promotion.

Traditional Dependency Injection in Drupal (Pre-PHP 8)

In Drupal services and controllers, the traditional pattern for dependency injection involves three steps:

  1. Declare each dependency as a class property (usually protected) with an appropriate docblock.

  2. Type-hint each dependency in the constructor parameters, and assign them to the class properties inside the constructor.

  3. For controllers (and some plugin classes), implement a static create(ContainerInterface $container) method to retrieve services from Drupal’s service container and instantiate the class.

This results in quite a bit of boilerplate code. For example, consider a simple custom service that needs the configuration factory and a logger factory. Traditionally, you would write:

Traditional Service Class Example

<?php

namespace Drupal\example;

/**
 * Example service that logs the site name.
 */
class ExampleService {
  /**
   * The configuration factory service.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The logger channel factory service.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerFactory;

  /**
   * Constructs an ExampleService object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The configuration factory.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger channel factory.
   */
  public function __construct(ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory) {
    // Store the injected services.
    $this->configFactory = $config_factory;
    $this->loggerFactory = $logger_factory;
  }

  /**
   * Logs the site name as an example action.
   */
  public function logSiteName(): void {
    $site_name = $this->configFactory->get('system.site')->get('name');
    $this->loggerFactory->get('example')->info('Site name: ' . $site_name);
  }
}

In the above, we declare two properties $configFactory and $loggerFactory and assign them in the constructor. The corresponding service must also be defined in the module’s services YAML (with the services it needs as arguments), for example:

# example.services.yml
services:
  example.example_service:
    class: Drupal\example\ExampleService
    arguments:
      - '@config.factory'
      - '@logger.factory'

When Drupal instantiates this service, it will pass the configured arguments to the constructor in the order listed.

Traditional Controller Class Example

Drupal controllers can also use dependency injection. Typically, a controller class extends ControllerBase (for convenience methods) and implements Drupal’s container injection by defining a create() method. The create() method is a factory that pulls services from the container and calls the constructor. For example:

<?php

namespace Drupal\example\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Controller for Example routes.
 */
class ExampleController extends ControllerBase {
  /**
   * The entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The string translation service.
   *
   * @var \Drupal\Core\StringTranslation\TranslationInterface
   */
  protected $stringTranslation;

  /**
   * Constructs an ExampleController object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
   *   The string translation service.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
    $this->entityTypeManager = $entity_type_manager;
    $this->stringTranslation = $string_translation;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): self {
    // Retrieve required services from the container and pass them to the constructor.
    return new self(
      $container->get('entity_type.manager'),
      $container->get('string_translation')
    );
  }

  /**
   * Builds a simple page response.
   */
  public function build(): array {
    // Example usage of the injected services.
    $node_count = $this->entityTypeManager->getStorage('node')->getQuery()->count()->execute();
    return [
      '#markup' => $this->t('There are @count nodes on the site.', ['@count' => $node_count]),
    ];
  }
}

Using Constructor Property Promotion (PHP 8+) in Drupal

Constructor property promotion streamlines the above pattern by letting you declare and assign properties in one step directly in the constructor signature. In PHP 8, you can prepend visibility (and other modifiers like readonly) to constructor parameters, and PHP will automatically create and assign those properties for you. This means you no longer need to separately declare the property or write the assignment inside the constructor – PHP does it for you.

Crucially, this is syntactic sugar. It doesn’t change how Drupal’s dependency injection works; it simply reduces the code you write. You still register services in YAML (or let Drupal autowire them), and for controllers you still use the create() factory method (unless you register the controller as a service). The difference is purely in how you write the class code. The result is significantly less boilerplate, as demonstrated by a Drupal core issue where dozens of lines of declarations and assignments were reduced to just a few lines in the constructor.

Let’s refactor our examples using constructor property promotion.

Service Class with Constructor Property Promotion

Here’s the ExampleService rewritten using the PHP 8 promoted properties syntax:

<?php

namespace Drupal\example;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;

/**
 * Example service that logs the site name (using property promotion).
 */
class ExampleService {
  /**
   * Constructs an ExampleService object with injected services.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The configuration factory.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger channel factory.
   */
  public function __construct(
    protected ConfigFactoryInterface $configFactory,
    protected LoggerChannelFactoryInterface $loggerFactory
  ) {
    // No body needed; properties are automatically set.
  }

  /**
   * Logs the site name as an example action.
   */
  public function logSiteName(): void {
    $site_name = $this->configFactory->get('system.site')->get('name');
    $this->loggerFactory->get('example')->info('Site name: ' . $site_name);
  }
}

Controller Class with Constructor Property Promotion

Now, consider the ExampleController refactored to use promoted properties:

<?php

namespace Drupal\example\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Controller for Example routes (using property promotion).
 */
final class ExampleController extends ControllerBase {
  /**
   * Constructs an ExampleController.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $stringTranslation
   *   The string translation service.
   */
  public function __construct(
    private EntityTypeManagerInterface $entityTypeManager,
    private TranslationInterface $stringTranslation
  ) {
    // No need for assignments; properties are set automatically.
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): self {
    // Pass services from the container into the constructor.
    return new self(
      $container->get('entity_type.manager'),
      $container->get('string_translation')
    );
  }

  /**
   * Builds a simple page response.
   */
  public function build(): array {
    $node_count = $this->entityTypeManager->getStorage('node')->getQuery()->count()->execute();
    return [
      '#markup' => $this->t('There are @count nodes on the site.', ['@count' => $node_count]),
    ];
  }
}

Advantages of Constructor Property Promotion

Using constructor property promotion in your Drupal classes offers several benefits:

  • Reduced Boilerplate: You write significantly less code. There is no need to manually declare properties and assign them, which can eliminate many lines especially when your class has multiple dependencies. This makes your modules cleaner and easier to maintain.

  • Clearer and More Concise Code: The class’s dependencies are all visible in one place – the constructor signature – rather than scattered between property declarations and constructor body. This improves readability and makes it immediately clear what services the class requires.

  • Fewer Doc Comments Needed: Because the properties are declared with types in the constructor, you may omit redundant @var and @param annotations for those properties and constructor parameters (provided the purpose is obvious from naming). The code is self-documenting to a large extent. You can still document anything non-obvious, but there's less repetition.

  • Modern PHP Syntax: Adopting property promotion means your code stays up-to-date with modern PHP practices. Drupal 10+ core has started using this syntax for new code, so using it in custom modules makes your code more consistent with the core and community examples. It also prepares your codebase for future enhancements (for example, PHP 8.1+ allows the use of the readonly keyword on promoted properties for truly immutable dependencies).

Performance and functionality remain the same as traditional injection – property promotion is purely a language convenience. You still get fully type-hinted properties that you can use throughout your class (e.g., $this->entityTypeManager in the controller example). Under the hood, the outcome is equivalent to the longer code; it's just achieved with less effort.

Conclusion

Constructor property promotion is a simple yet powerful PHP 8 feature that Drupal developers can leverage to simplify custom module development. By eliminating boilerplate, it lets you focus on what your class actually does rather than the wiring of services. We showed how to convert a typical Drupal service class and controller class to use promoted properties, and compared it with the traditional approach. The result is more concise and maintainable code without sacrificing clarity or functionality. As Drupal moves forward with modern PHP requirements, using features like property promotion in your custom modules will help keep your code clean, clear, and in line with current best practices. Embrace the modern syntax to make your Drupal development both easier and more elegant.