Attribute-based plugins

Last updated on
11 August 2025

Drupal 10.2.0+

PHP Attributes for plugin discovery were introduced in Drupal 10.2.0. For backwards compatibility PHP annotations are still supported.

Version compatibility

Starting in Drupal 10.2.0, plugins should be defined with PHP attributes instead of annotations. As of 10.3/11.0 most core plugin instances have been converted to use PHP attributes and can be referred to as examples.

When defining new plugin instances you should prefer PHP Attributes over annotations assuming the underlying plugin manager has been updated to support the use of attributes. Check existing plugins provided by core, or the module that implements the plugin manager, to determine if PHP Attributes can be used.

Modules that provide new plugin types using PHP annotations for discovery, and plugin instances using annotations, will continue to be supported for now but should be updated to using PHP attributes.

To convert an existing plugin manager refer to Plugin types should use PHP attributes instead of annotations and to convert plugin instances refer to Plugin implementations should use PHP attributes instead of annotations.

Defining a plugin instance

Plugins are registered according to the PSR-4 standard. Place your plugin file in one of the following locations:

  • src/Plugin/my_plugin_type/plugin_name.php
  • src/Plugin/custom_vendor/my_plugin_type/plugin_name.php

For example:

  • A CKEditor plugin might be registered as core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
  • A custom views argument handler might be at core/modules/ckeditor/src/Plugin/views/argument/Fid.php

Defining a plugin

Define a plugin by adding class that implements the plugin type specific interface, and placing a PHP attribute before the class definition, similar to how annotations were previously used. Here’s how you might define a Block plugin.

File: custom_module/src/Plugin/Block/CustomBlock.php:

<?php

namespace Drupal\custom_module\Plugin\Block;

use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
 * Provides a custom block plugin.
 */
#[Block(
  id: "custom_block",
  admin_label: new TranslatableMarkup("Custom Block"),
  category: new TranslatableMarkup("Custom Category"),
)]
class CustomBlock extends BlockBase {
  // Plugin implementation.
}

In this example:

  • The plugin is placed into the Drupal\$module_name\Plugin\Block namespace so the plugin manager can find it.
  • The class has a Drupal\Core\Block\Attribute\Block attribute so the plugin manager can read meta-data about the plugin instance.
  • The class CustomBlock extends the plugin type specific base class BlockBase (most plugin types have a base class you can extend) and implements the \Drupal\Core\Block\BlockPluginInterface.

For more details about what meta-data can be provided in a plugin's attributes see the documentation for the __construct() method of the attribute class. Here's the code from Drupal\Core\Block\Attribute\Block:

/**
 * 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\Plugin\Context\ContextDefinitionInterface[] $context_definitions
 *   (optional) An array of context definitions describing the context used by
 *   the plugin. The array is keyed by context names.
 * @param class-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 = [],
) {}

This provides details about the expected values for the attribute's arguments, potential default values, and information about how the collected meta-data will be used.

Using attributes in your own plugin type

If you write your own plugin type and want to use attributes, start by extending the DefaultPluginManager, which currently supports both Attributes and AnnotatedClassDiscovery. Then:

  • Define the sub-namespace / sub-directory in which plugins of this type will be located.
  • Define the attributes class. Read more about this in Create your own custom attribute class.
  • Create a new service that extends the DefaultPluginManager and wire it up your plugin type specific details.

For example, the Drupal core FormatterPluginManager which manages field formatter instances has the following:

  • Namespace: $module/src/Plugin/Field/FieldFormatter
  • Attribute class: Drupal\Core\Field\Attribute\FieldFormatter

The FormatterPluginManager::_construct() method calls the parent class DefaultPluginManager constructor and in doing so sets the namespace to search for plugin instances via the 1st argument, and attribute class to use as the 5th argument.

Example:

use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Plugin\DefaultPluginManager;

class FormatterPluginManager extends DefaultPluginManager {

  /**
   * Constructs a FormatterPluginManager object.
   */
 public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, FieldTypePluginManagerInterface $field_type_manager) {
    parent::__construct('Plugin/Field/FieldFormatter', $namespaces, $module_handler, 'Drupal\Core\Field\FormatterInterface', FieldFormatter::class);
    $this->setCacheBackend($cache_backend, 'field_formatter_types_plugins');
    $this->alterInfo('field_formatter_info');
    $this->fieldTypeManager = $field_type_manager;
  }
}

The additional injected $namespaces come from the dependency injection container. For example, the FieldBundle:

/**
 * @file
 * Contains Drupal\field\FieldBundle.
 */
namespace Drupal\field;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

/**
 * Field dependency injection container.
 */
class FieldBundle extends Bundle {

  /**
   * Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
   */
  public function build(ContainerBuilder $container) {
    // Register the plugin managers for our plugin types with the dependency injection container.
    $container->register('plugin.manager.field.widget', 'Drupal\field\Plugin\Type\Widget\WidgetPluginManager')
      ->addArgument('%container.namespaces%');
    $container->register('plugin.manager.field.formatter', 'Drupal\field\Plugin\Type\Formatter\FormatterPluginManager')
      ->addArgument('%container.namespaces%');
  }
}

To call your custom plugin manager, you'll need to inject Drupal's namespaces into the class construction call.

$type = new CustomPluginManager(\Drupal::getContainer()->getParameter('container.namespaces'));

Help improve this page

Page status: No known problems

You can: