Attributes In Drupal

December 18, 2023 - 5 min read

Drupal 10.2 is out, with an easier content management by improving user experience, and with some performance improvements in caching and HTTP responses. It is also compatible with PHP 8.3, and it started using PHP attributes.

TL;DR; - Jump to the 'How to' section

What are attributes in PHP?

They introduce a flexible and standardized approach to add metadata to your code — similar to Doctrine annotations —, enhancing readability and documentation. Attributes are declared using the #[...] syntax, positioned above the element to which you want to attach them. These can also take arguments, allowing you to convey specific information. For instance:

#[ExampleAttribute('argument')]
class ExampleClass {
    // Class code here.
}

To retrieve information about a class which is using attributes, the Reflection API comes to the rescue. Using its getAttributes() method, you'll find yourself with an array of ReflectionAttribute objects:

$class = new ReflectionClass(ExampleClass::class);
$attributes = $class->getAttributes();

/** @var \ReflectionAttribute $attribute */
foreach ($attributes as $attribute) {
    $name = $attribute->getName(); // 'ExampleAttribute'
    $arguments = $attribute->getArguments(); // ['argument'] 
}

Drupal uses a new class to parse attributes (e.g. from a Block plugin) based on this API, which we will talk about later.

Handling Sensitive Data (Use Case)

In PHP 8.2 a new attribute called #[\SensitiveParameter] has been introduced. This attribute proves to be invaluable when dealing with sensitive information in stack traces generated for exceptions. It allows for the redaction of specific parameters, such as passwords, ensuring enhanced security when debugging, or when looking at logs.

Note: Since PHP 7.4 the zend.exception_ignore_args = On setting is available for use which allows to include or exclude arguments from stack traces generated for exceptions.

For example, PDO uses the $password as a constructor parameter, and immediately tries to connect to the database. When this fails, the stack trace will include the password:

PDOException: SQLSTATE[HY000] [2002] No such file or directory in /var/www/html/test.php:3
Stack trace:
#0 /var/www/html/test.php(3): PDO->__construct('mysql:host=loca...', 'root', 'password')
#1 {main}

When those sensitive parameters are marked by the new attribute, their value will be redacted, meaning instead of 'password', we should receive an Object(SensitiveParameterValue) in the stack trace.

More information about the attribute can be found in the RFC.

Why Change From Doctrine Annotations?

Doctrine Annotations are primarily used by Doctrine ORM, and they already started to migrate to PHP attributes. Consequently, annotations might face deprecation sooner or later. Given that Drupal utilizes Doctrine Annotations solely for metadata, and with PHP 8 providing a built-in approach for this purpose (further enhanced by PHP 8.1 readonly properties), Drupal has the option to remove the dependency on Doctrine Annotations for good.

Does this mean I can't use annotations anymore?

Drupal 10.2 lets you use attributes to declare custom classes, methods, parameters, properties or constants, but it does not explicitly use them to maintain backwards compatibility. This means contributed and custom modules can start migrating their code, aligning with the evolving standards (something that Drush 11 already did) before Drupal 11.

This change will lead to:

How does the code change

Let's see Drupal Core's PageTitleBlock for our example, which in Drupal 10.1, uses annotations:

<?php

namespace Drupal\Core\Block\Plugin\Block;

use ...

/**
 * Provides a block to display the page title.
 *
 * @Block(
 *   id = "page_title_block",
 *   admin_label = @Translation("Page title"),
 *   forms = {
 *     "settings_tray" = FALSE,
 *   },
 * )
 */
class PageTitleBlock extends BlockBase implements TitleBlockPluginInterface {

However, in Drupal 10.2 it uses PHP attributes.

<?php

namespace Drupal\Core\Block\Plugin\Block;

use ...

/**
 * Provides a block to display the page title.
 */
#[Block(
  id: "page_title_block",
  admin_label: new TranslatableMarkup("Page title"),
  forms: [
    'settings_tray' => FALSE,
  ]
)]
class PageTitleBlock extends BlockBase implements TitleBlockPluginInterface {

The change record says currently all Actions and Blocks are converted to use attributes, but what if I am a contrib/custom module developer and I define custom plugins?

If we carefully inspect the DefaultPluginManager in Drupal 10.2, we notice there is a new constructor parameter called $plugin_definition_attribute_name which is initially NULL. This means, you'll need to add a new parameter to the parent constructor call (at least until $plugin_definition_annotation_name parameter is still in use and not deprecated.)

If we check the BlockManager class, you can see the parent constructor call uses Block::class as the 5th parameter in 10.2

// Before Drupal 10.2
parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\Core\Block\BlockPluginInterface', 'Drupal\Core\Block\Annotation\Block');

// After
parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\Core\Block\BlockPluginInterface', Block::class, 'Drupal\Core\Block\Annotation\Block');

This parameter is then stored in the $pluginDefinitionAttributeName property, and that gets used later in the getDiscovery() method of DefaultPluginManager.

As you can see, this method then decides if it should use:

Thoughts

When creating new plugins from now on, developers should primarily focus on using Attributes in the plugin definitions instead of Doctrine Annotations and ensuring that the appropriate parameters are included in the parent constructor call. The good news is that the process of registering the plugin in the services.yml file remains unchanged, allowing the developers to continue using parent: default_plugin_manager.

While the changes may seem subtle, the adoption of PHP attributes opens up new possibilities leading to a more standardized, advanced way of code organization and metadata handling.


Profile picture

Written by Balint Pekker, a software engineer being really good at turning caffeine into code.