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.
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:
- Possibly a deprecated AnnotatedClassDiscovery removed in Drupal 11.
- A new AttributeClassDiscovery using Reflection API to find plugins with attributes (introduced in Drupal 10.2) - There is an active Drupal 11 issue to investigate possibilities on how to improve performance by using something different than the Reflection API.
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:
- AttributeDiscoveryWithAnnotations
- AttributeClassDiscovery
- AnnotatedClassDiscovery
- or ContainerDerivativeDiscoveryDecorator
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.