Skip to content

How-to guideFor module developersDrupal 11

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.

  • 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.
  1. Find the attribute class for your plugin type. Every annotation has an attribute counterpart in an Attribute namespace beside the old Annotation one: \Drupal\Core\Block\Annotation\Block becomes \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.

  2. Rewrite the docblock as an attribute. Three things change together: the @Name(...) docblock becomes #[Name(...)] directly above the class, the key = value pairs become key: value (PHP named arguments, comma-separated), and any @Translation("...") value becomes new TranslatableMarkup('...'). Add use statements for the attribute class and, where you used @Translation, for TranslatableMarkup.

    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 @Translation anywhere in the definition becomes a new TranslatableMarkup(...) call.

  3. 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.

  • ddev drush cache:rebuild completes 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.

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.

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.

drupal-rector rewrites core plugin annotations as attributes in bulk. Run it, review the diff, then rebuild and verify as above.

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.