By james, 21 December, 2023
PHP Attributes symbol

It’s day 21, and our stockings are warming over the fireplace. Joe Shindelar (eojthebrave) joins us to open today’s door, and for the PHP developers among us, it will make a nice little stocking filler.

As of PHP 8.1, the PHP language has native support for attributes that are compatible with Drupal’s plugin system use case. As a result, Drupal will transition from the use of annotations to PHP attributes, to supply metadata and configuration for plugins. This will require developers to learn the new PHP attributes syntax, and update their existing code to use it. For now Drupal will continue to support both annotations and attributes. But the proverbial clock is ticking.

So let’s take a look at how we got here, and what you’ll need to do to update your code for future versions of Drupal.

Drupal plugins

Since the release of Drupal 8, the plugin system has used annotations to allow metadata and configuration for a plugin instance to be collected. Plugin managers use this data to do things like make a list of plugins of a given type, or to retrieve the human-readable label for a plugin, in an efficient manner. The system supports other discovery mechanisms, but annotations are the most commonly used, and the one that module developers are most likely to encounter when authoring custom code.

Annotations are great because the metadata lives in the same file as the PHP class that implements the plugin, making them easier to discover, and improving the overall developer experience. Annotations are a common feature in many programming languages. But prior to PHP 8.0 there was not support for them in PHP. So Drupal (and many other PHP projects) use the popular doctrine/annotation library as a shim.

Attributes were first added to PHP in 8.0, but the initial version did not support nested attributes. Something that is vital to making them work for Drupal. That feature was later added in PHP 8.1, which opened the doors for Drupal to switch from using annotations to using PHP-native attributes to accomplish the same goals.

As the PHP language evolves, so, too, should Drupal.

Attributes versus annotations

Being able to add metadata about the code, alongside the code, is a common need. So much so that when PHP didn’t support it natively, user-space solutions to the problem arose. The doctrine/annotations package is the most widely used solution. And it called this feature annotations.

The popularity of user-land annotations lead to the introduction of multiple RFCs [Attributes, Attributes V2] and the eventual adoption of the feature into PHP core. In order to minimize confusion with the user-land implementation, the language feature is called attributes instead of annotations.

Some notable differences between attributes and annotations:

  • Annotations are interpreted at runtime. The extra parsing step adds overhead which can impact performance. Especially if there are a lot of annotations. Though Drupal mostly mitigates this by caching the plugin discovery process. Attributes, on the other hand, are resolved at compile-time, not runtime. And are a native feature of the language. This can lead to faster performance, especially in environments where an opcache is used
  • Since annotations are a bit of a hack on top of the phpdoc syntax they clutter comments, which should be for humans, with metadata
  • Annotations are prone to bugs due to little nuances. For example, an annotation needs to occur last in the docblock comment. If a phpdoc tag like @see occurs after the annotation, the parser will get confused
  • IDE support for native language features will be more widespread

Still not convinced? The maintainers of the defacto PHP annotation library doctrine/annotations believe that native PHP attributes are so much better than the current annotation systems that the library has been deprecated and is no longer being actively maintained. Which is also part of why this change is important for Drupal. We don’t want to be reliant on deprecated, unsupported code.

Attributes for Drupal developers

From a Drupal developer’s perspective, attributes accomplish the same goals, and are used the same way as annotations, though the syntax is different. Let’s take a look at some examples.

The following is an example of the annotation used in Drupal 10.1 to define the system branding block plugin:

<?php

namespace Drupal\system\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a block to display 'Site branding' elements.
 *
 * @Block(
 *   id = "system_branding_block",
 *   admin_label = @Translation("Site branding"),
 *   forms = {
 *     "settings_tray" = "Drupal\system\Form\SystemBrandingOffCanvasForm",
 *   },
 * )
 */
class SystemBrandingBlock extends BlockBase implements ContainerFactoryPluginInterface {}

Here’s the same block plugin definition from Drupal 10.2 using attributes:

<?php

namespace Drupal\system\Plugin\Block;

use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\system\Form\SystemBrandingOffCanvasForm;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a block to display 'Site branding' elements.
 */
#[Block(
  id: "system_branding_block",
  admin_label: new TranslatableMarkup("Site branding"),
  forms: ['settings_tray' => SystemBrandingOffCanvasForm::class]
)]
class SystemBrandingBlock extends BlockBase implements ContainerFactoryPluginInterface {}

At first glance, we can see a couple of key syntax differences. The use of #[ ... ] to declare an attribute versus the /** ... */ (docblock delimiter) used for annotations. This makes it clear that the metadata and the comment above it are two distinct things. I love this about attributes. In my IDE I even get syntax highlighting for the attributes.

Chef’s kiss 😘.

Screenshot showing side by side comparison of annotation and attribute
Example of how annotations and attributes are highlighted in an IDE

I included the use statements in my before and after examples because an important distinction between annotations and attributes is that you have to include a use statement for the attributes class use Drupal\Core\Block\Attribute\Block; as well as the other classes you use in the attribute declaration like use Drupal\Core\StringTranslation\TranslatableMarkup;. The annotation system doesn’t have that requirement. A little more work, but it also has the benefit of making it totally clear which annotation class is being used, and I can use my IDE’s built-in code navigation tools to quickly locate the class and its documentation.

Arguments to attributes can only be literal values or constant expressions (values that can be calculated at compile time). You can read more about the syntax in the official documentation.

Aside, since this was the first time I’ve seen them used in the real world, PHP (as of 8.0) supports named parameters. That’s why passing arguments to the attribute constructor like id: "system_branding_block" works. And allows for arguments to be passed in any order.

Defining custom attribute types

In Drupal, every plugin type will have its own attribute type. As a developer you’ll encounter them in two scenarios:

  1. You need to implement a plugin of a specific type and that requires declaring the correct attribute
  2. You’re creating a new plugin type, and associated plugin manager, and will need to define the attribute type and use it in a plugin manager

Attributes are retrieved using the PHP Reflection API. At compile time, attributes are parsed and their data is stored as internal structures. This can be retrieved using ReflectionClass::getAttributes(), and essentially amounts to an array of ReflectionAttribute items. You can think of this as an array containing the data from the attribute declaration. A Drupal plugin manager can use this information to get the plugin id or label.

Attributes are associated with classes, and what they contain. And just like annotations, should be documented on that class. So when you’re ready to implement a plugin you’ll first need to figure out what attribute to use, and thus which class contains the documentation. As with the current system, this will probably be easiest to figure out by looking at an existing plugin implementation and learning the attribute name from there.

When it comes to converting annotations to attributes, I would guess that in most cases the name will remain the same. For example @Block() becomes [#Block()] and @ContentEntity() becomes [#ContentEntity()].

Below is the code for Drupal\Core\Block\Attribute\Block. Which is the attribute that will be used to define a Block plugin.

<?php

namespace Drupal\Core\Block\Attribute;

use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
 * The Block attribute.
 */
#[\Attribute(\Attribute::TARGET_CLASS)]
class Block extends Plugin {

  /**
   * Constructs a Block attribute.
   *
   * @param string $id
   *   The plugin ID.
   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $admin_label
   *   The administrative label of the block.
   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $category
   *   (optional) The category in the admin UI where the block will be listed.
   * @param \Drupal\Core\Annotation\ContextDefinition[] $context_definitions
   *   (optional) An array of context definitions describing the context used by
   *   the plugin. The array is keyed by context names.
   * @param string|null $deriver
   *   (optional) The deriver class.
   * @param string[] $forms
   *   (optional) An array of form class names keyed by a string.
   */
  public function __construct(
    public readonly string $id,
    public readonly ?TranslatableMarkup $admin_label = NULL,
    public readonly ?TranslatableMarkup $category = NULL,
    public readonly array $context_definitions = [],
    public readonly ?string $deriver = NULL,
    public readonly array $forms = []
  ) {}

}

There’s a couple of things that stood out to me when I first read this code:

  • #[\Attribute(\Attribute::TARGET_CLASS)]: This line effectively says that the class Block defines a #[Block()] attribute, and that the block attribute can only be used on classes. Or put another way, the code that the #[Block()] attribute is annotating must be a class, or it won’t validate
  • The docblock comment for the __construct() method documents the attribute’s parameters. Explaining all the required and optional inputs for the attribute

Using an attribute is analogous to instantiating an object using the new keyword. It’s the class name, Block, with a pair of parentheses () which contain any arguments for the class constructor. Using that knowledge, I can see from the code above, that a [#Block()] Attribute requires an id parameter, and will accept optional admin_label, category, context_definitions, deriver, and forms arguments. I also like that this makes it clear that the admin_label argument expects a TranslatableMarkup object as its value, and that forms needs to be an array.
I do wish the documentation for forms was a little more detailed though. It’s not clear to me how someone would know that forms['settings_tray'] is a useful key but forms['cool_form_one'] isn’t. Though it’s not any worse than it was with annotations at least.

Updating a plugin manager to use a new attribute type

To update a plugin manager to use a new attribute type, first introduce the attribute object by creating a new class. The attribute class will be a 1-to-1 mapping of the keys used in the annotation to the parameters names in the attribute. That’s not a strict requirement, but it’ll make it a lot easier for anyone implementing your plugin type to update their plugin instances.

Don’t remove the old annotation class yet. For now, Drupal will support both annotations and attributes for discovery.

Most plugin managers will extend \Drupal\Core\Plugin\DefaultPluginManager, and override the __construct() method. The new constructor will call the parent class’s constructor and in doing so, tell the plugin manager how to discover plugins of the given type, and what class to use for both attributes and annotations. You’ll want to update your overridden constructor method so that it passes the attribute class you added above and the annotation class to the parent. The DefaultPluginManager will work with and without attribute discovery for now.

“Before” example:

public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, LoggerInterface $logger) {
    parent::__construct(
			'Plugin/Block',
			$namespaces,
			$module_handler,
			'Drupal\Core\Block\BlockPluginInterface',
			Block::class,
			'Drupal\Core\Block\Annotation\Block'
		);

		// Additional unchanged code ...
  }

“After” example:

public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, LoggerInterface $logger) {
    parent::__construct(
			'Plugin/Block',
			$namespaces,
			$module_handler,
			'Drupal\Core\Block\BlockPluginInterface',
			'Drupal\Core\Block\Annotation\Block'
		);

		// Additional unchanged code ...
  }

After making this change your plugin manager will support both annotations and attributes for discovery. Once there’s a more concrete plan for deprecating annotations (see below), I anticipate there will also be additional directions about how to handle this in your custom plugin manager code if necessary.

If you really want to get into the weeds see \Drupal\Core\Plugin\Discovery\AttributeDiscoveryWithAnnotations.

When should I start using attributes instead of annotations?

You’re probably wondering, can I use attributes right now? Should I use attributes right now? Do I have to use attributes right now?

Personally, at a minimum, I’ll wait until Drupal rector support is complete and the long-term deprecation plans have been sorted out before I start converting any of my projects.

The change from annotations to attributes, while relatively simple, is daunting in its scale. No doubt some of you will want to get started updating your code right away, and others might be a bit worried about what this will mean for long-term maintenance. So here’s where things are at today.

Attributes requires Drupal 10.2.0+

The ability to use attributes for plugins was first introduced in Drupal 10.2.0. So at a minimum any code using attributes will have to be in a Drupal 10.2+ project. Which at the time of this writing hasn’t been released. [It’s here!]

And, even when Drupal 10.2 is released, only some of the core plugin types will have been converted to use attributes for discovery (Blocks and Actions, as of right now). This suggests that while you should get up to speed on attributes and how to use them, you won’t really be able to use them until all (or most) of core’s plugin types are converted.

This will take time, and with it being relatively late in the Drupal 10 cycle, it’s possible that deprecating annotations might not happen until Drupal 11, with support removed in Drupal 12.

On the flip side, doctrine/annotations is already deprecated. And in the time it takes to release Drupal 12 (2028/29), it could likely be EOL. Which would put the Drupal community on the hook to continue to support the annotation parsing library. Or at least the parts of it we use.

Follow this issue to keep up with the plan: [policy, no patch] Allow both annotations and attributes in Drupal 11].

My gut says annotations will continue to be supported alongside attributes for while.

Drupal rector support is coming

Batteries included. It’s not complete yet, but there is ongoing work to update the Drupal rector project to be able to automatically convert your code’s Doctrine annotations to PHP attributes. Once that is complete, it’s likely this functionality could also be built into the Project Update Bot. Which will help ease, though not totally alleviate, the update burden.

There also isn’t yet anything in the Drupal coding standards about attributes. Though there’s an open issue: [Docment use PHP Attributes for plugins], and I suspect that we’ll get some standards around formatting in the future.

Recap

Drupal is transitioning from annotations to native PHP attributes for plugin discovery and configuration. In this article, we learned that:

  • PHP attributes are a native implementation of a solution to the same problem that annotations solved. They’re faster, have better developer experience, and in the long term, will be better supported
  • As a Drupal developer, you’ll need to learn how to use PHP attributes to define new Drupal plugins. While the switch requires learning a new syntax, the overall application remains similar. It should feel familiar to anyone who has previously used annotations
  • Support for PHP attributes was added in Drupal 10.2 and is still evolving. For now, developers should be aware that the change is coming, but don’t need to jump to immediately trying to convert all their code. Instead, wait for a more detailed deprecation plan, and for Drupal rector to provide support for assisting with updates

The goal is to ensure a smooth transition that benefits the Drupal community and keeps Drupal compatible with the evolving PHP language.

Additional Resources

Photo of Joe ShindelarJoe Shindelar is the lead trainer for Drupalize.Me, so likes to think of ways to make Drupal easier to learn. He is also a member of the Drupal Documentation Working Group, and has contributed to improving Drupal documentation. Away from the keyboard, he is a keen snowboarder and even a qualified snowboard instructor.

Comments

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.