Replace annotations with PHP attributes
Drupal 11 declares plugins with PHP attributes. The older form, a Doctrine-style annotation in the class docblock (@Block, @FieldType, @QueueWorker, and the rest), still works, so a module written for Drupal 9 or 10 keeps running. But core has converted its own plugins, attributes are what new code uses, and the annotation classes will not last forever. This page converts a plugin from one to the other.
If you are writing a new plugin, skip the conversion and start from the attribute form shown in Plugin attributes and hooks.
A plugin class whose declaration is a #[...] attribute above the class instead of a /** @... */ docblock, with the same plugin ID, the same definition values, and the same discovery result. Nothing about where the file lives or what it extends changes; only the declaration syntax does.
Prerequisites
Section titled “Prerequisites”- Plugins for how discovery and plugin managers work.
- Plugin attributes and hooks for the attribute class that matches your plugin type and the directory it lives in.
- A Drupal 11 site and a plugin class that still uses an annotation.
-
Find the attribute class for your plugin type. Every annotation has an attribute counterpart in an
Attributenamespace beside the oldAnnotationone:\Drupal\Core\Block\Annotation\Blockbecomes\Drupal\Core\Block\Attribute\Block, and so on. The table in Plugin attributes and hooks lists the common ones; contrib plugin types follow the same pattern in their own namespace. -
Rewrite the docblock as an attribute. Three things change together: the
@Name(...)docblock becomes#[Name(...)]directly above the class, thekey = valuepairs becomekey: value(PHP named arguments, comma-separated), and any@Translation("...")value becomesnew TranslatableMarkup('...'). Addusestatements for the attribute class and, where you used@Translation, forTranslatableMarkup.Before, the legacy annotation form:
namespace Drupal\event_rsvp\Plugin\Block;use Drupal\Core\Block\BlockBase;/*** @Block(* id = "event_rsvp_count",* admin_label = @Translation("RSVP count"),* )*/final class RsvpCountBlock extends BlockBase {// ...}After, the same plugin as an attribute:
namespace Drupal\event_rsvp\Plugin\Block;use Drupal\Core\Block\Attribute\Block;use Drupal\Core\Block\BlockBase;use Drupal\Core\StringTranslation\TranslatableMarkup;#[Block(id: 'event_rsvp_count',admin_label: new TranslatableMarkup('RSVP count'),)]final class RsvpCountBlock extends BlockBase {// ...}Nested definition values follow the same rule. An array stays an array (
['node' => 'node']), and a nested@Translationanywhere in the definition becomes anew TranslatableMarkup(...)call. -
Rebuild so the plugin manager rediscovers the class:
ddev drush cache:rebuild. Discovery is cached, so the attribute is not seen until you clear it.
Verify
Section titled “Verify”ddev drush cache:rebuildcompletes without error.- The plugin still appears where it did before: a block in Administer → Structure → Block layout, a field type in the field add form, and so on. The plugin ID did not change, so existing configuration that references it keeps working.
- Search the module for the annotation that you converted (for example
grep -rn "@Block" web/modules/custom/event_rsvp); the docblock declaration is gone.
Variations
Section titled “Variations”Entity types
Section titled “Entity types”Content and config entity types are declared the same way, with \Drupal\Core\Entity\Attribute\ContentEntityType or ConfigEntityType on the entity class, and the same @Translation to TranslatableMarkup swap applies to labels. Custom entity types shows the attribute form in full.
A module with many plugins
Section titled “A module with many plugins”Convert one plugin type at a time and rebuild between them, rather than the whole module at once. Annotated and attribute-based plugins coexist in the same module without conflict, so a partial conversion is always in a working state.
Automating the conversion
Section titled “Automating the conversion”drupal-rector rewrites core plugin annotations as attributes in bulk. Run it, review the diff, then rebuild and verify as above.
Related
Section titled “Related”- Plugins for the discovery system behind both forms.
- Plugin attributes and hooks for the full attribute lookup table.
- Convert procedural hooks to classes for the related move from
.modulefunctions to#[Hook]classes. - The
plugin API topicon api.drupal.org.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.