diff --git a/core/lib/Drupal/Component/Plugin/Attribute/AttributeBase.php b/core/lib/Drupal/Component/Plugin/Attribute/AttributeBase.php new file mode 100644 index 0000000000000000000000000000000000000000..7ebc93e69c428a2d3eb65593108bef29a1c4a1d9 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Attribute/AttributeBase.php @@ -0,0 +1,77 @@ +<?php + +namespace Drupal\Component\Plugin\Attribute; + +/** + * Provides a base class for classed attributes. + */ +abstract class AttributeBase implements AttributeInterface { + + /** + * The class used for this attribute class. + * + * @var class-string + */ + protected string $class; + + /** + * The provider of the attribute class. + */ + protected string|null $provider = NULL; + + /** + * @param string $id + * The attribute class ID. + */ + public function __construct( + protected readonly string $id + ) {} + + /** + * {@inheritdoc} + */ + public function getProvider(): ?string { + return $this->provider; + } + + /** + * {@inheritdoc} + */ + public function setProvider(string $provider): void { + $this->provider = $provider; + } + + /** + * {@inheritdoc} + */ + public function getId(): string { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function getClass(): string { + return $this->class; + } + + /** + * {@inheritdoc} + */ + public function setClass(string $class): void { + $this->class = $class; + } + + /** + * {@inheritdoc} + */ + public function get(): array|object { + return array_filter(get_object_vars($this) + [ + 'class' => $this->getClass(), + 'provider' => $this->getProvider(), + ], function ($value, $key) { + return !($value === NULL && ($key === 'deriver' || $key === 'provider')); + }, ARRAY_FILTER_USE_BOTH); + } + +} diff --git a/core/lib/Drupal/Component/Plugin/Attribute/AttributeInterface.php b/core/lib/Drupal/Component/Plugin/Attribute/AttributeInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..30964398a90f0e071f712f0a8254005b58280452 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Attribute/AttributeInterface.php @@ -0,0 +1,52 @@ +<?php + +namespace Drupal\Component\Plugin\Attribute; + +/** + * Defines a common interface for classed attributes. + */ +interface AttributeInterface { + + /** + * Gets the value of an attribute. + */ + public function get(): mixed; + + /** + * Gets the name of the provider of the attribute class. + * + * @return string|null + */ + public function getProvider(): ?string; + + /** + * Sets the name of the provider of the attribute class. + * + * @param string $provider + * The provider of the annotated class. + */ + public function setProvider(string $provider): void; + + /** + * Gets the unique ID for this attribute class. + * + * @return string + */ + public function getId(): string; + + /** + * Gets the class of the attribute class. + * + * @return class-string|null + */ + public function getClass(): ?string; + + /** + * Sets the class of the attributed class. + * + * @param class-string $class + * The class of the attributed class. + */ + public function setClass(string $class): void; + +} diff --git a/core/lib/Drupal/Component/Plugin/Attribute/Plugin.php b/core/lib/Drupal/Component/Plugin/Attribute/Plugin.php new file mode 100644 index 0000000000000000000000000000000000000000..af98c93c6a257e0d3441a024fcf35cea2045f290 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Attribute/Plugin.php @@ -0,0 +1,30 @@ +<?php + +namespace Drupal\Component\Plugin\Attribute; + +/** + * Defines a Plugin attribute object. + * + * Attributes in plugin classes can use this class in order to pass various + * metadata about the plugin through the parser to + * DiscoveryInterface::getDefinitions() calls. + * + * @ingroup plugin_api + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class Plugin extends AttributeBase { + + /** + * Constructs a plugin attribute object. + * + * @param string $id + * The attribute class ID. + * @param class-string|null $deriver + * (optional) The deriver class. + */ + public function __construct( + public readonly string $id, + public readonly ?string $deriver = NULL + ) {} + +} diff --git a/core/lib/Drupal/Component/Plugin/Attribute/PluginID.php b/core/lib/Drupal/Component/Plugin/Attribute/PluginID.php new file mode 100644 index 0000000000000000000000000000000000000000..66368d0546dc348a733ee84b56497f0206237c1b --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Attribute/PluginID.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\Component\Plugin\Attribute; + +/** + * Defines a Plugin attribute object that just contains an ID. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class PluginID extends AttributeBase { + + /** + * {@inheritdoc} + */ + public function get(): array { + return [ + 'id' => $this->getId(), + 'class' => $this->getClass(), + 'provider' => $this->getProvider(), + ]; + } + +} diff --git a/core/lib/Drupal/Component/Plugin/Discovery/AttributeBridgeDecorator.php b/core/lib/Drupal/Component/Plugin/Discovery/AttributeBridgeDecorator.php new file mode 100644 index 0000000000000000000000000000000000000000..3f354693e95b065f2cba16a1eabdaf529674d44b --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Discovery/AttributeBridgeDecorator.php @@ -0,0 +1,69 @@ +<?php + +namespace Drupal\Component\Plugin\Discovery; + +/** + * Ensures that all definitions are run through the attribute process. + */ +class AttributeBridgeDecorator implements DiscoveryInterface { + + use DiscoveryTrait; + + /** + * AttributeBridgeDecorator constructor. + * + * @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated + * The discovery object that is being decorated. + * @param string $pluginDefinitionAttributeName + * The name of the attribute that contains the plugin definition. The class + * corresponding to this name must implement + * \Drupal\Component\Plugin\Attribute\AttributeInterface. + */ + public function __construct( + protected readonly DiscoveryInterface $decorated, + protected readonly string $pluginDefinitionAttributeName + ) {} + + /** + * {@inheritdoc} + */ + public function getDefinitions() { + $definitions = $this->decorated->getDefinitions(); + foreach ($definitions as $id => $definition) { + // Attribute constructors expect an array of values. If the definition is + // not an array, it usually means it has been processed already and can be + // ignored. + if (is_array($definition)) { + $class = $definition['class'] ?? NULL; + $provider = $definition['provider'] ?? NULL; + unset($definition['class'], $definition['provider']); + /** @var \Drupal\Component\Plugin\Attribute\AttributeInterface $attribute */ + $attribute = new $this->pluginDefinitionAttributeName(...$definition); + if (isset($class)) { + $attribute->setClass($class); + } + if (isset($provider)) { + $attribute->setProvider($provider); + } + $definitions[$id] = $attribute->get(); + } + } + return $definitions; + } + + /** + * Passes through all unknown calls onto the decorated object. + * + * @param string $method + * The method to call on the decorated plugin discovery. + * @param array $args + * The arguments to send to the method. + * + * @return mixed + * The method result. + */ + public function __call($method, $args) { + return $this->decorated->{$method}(...$args); + } + +} diff --git a/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php b/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php new file mode 100644 index 0000000000000000000000000000000000000000..9accdefd55e1c02da794521dd0a1b3bbfb94177f --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php @@ -0,0 +1,160 @@ +<?php + +namespace Drupal\Component\Plugin\Discovery; + +use Drupal\Component\Plugin\Attribute\AttributeInterface; +use Drupal\Component\Plugin\Attribute\Plugin; +use Drupal\Component\FileCache\FileCacheFactory; +use Drupal\Component\FileCache\FileCacheInterface; + +/** + * Defines a discovery mechanism to find plugins with attributes. + */ +class AttributeClassDiscovery implements DiscoveryInterface { + + use DiscoveryTrait; + + /** + * The file cache object. + */ + protected FileCacheInterface $fileCache; + + /** + * Constructs a new instance. + * + * @param string[] $pluginNamespaces + * (optional) An array of namespace that may contain plugin implementations. + * Defaults to an empty array. + * @param string $pluginDefinitionAttributeName + * (optional) The name of the attribute that contains the plugin definition. + * Defaults to 'Drupal\Component\Plugin\Attribute\Plugin'. + */ + public function __construct( + protected readonly array $pluginNamespaces = [], + protected readonly string $pluginDefinitionAttributeName = Plugin::class + ) { + $file_cache_suffix = str_replace('\\', '_', $this->pluginDefinitionAttributeName); + $this->fileCache = FileCacheFactory::get('attribute_discovery:' . $this->getFileCacheSuffix($file_cache_suffix)); + } + + /** + * Gets the file cache suffix. + * + * This method allows classes that extend this class to add additional + * information to the file cache collection name. + * + * @param string $default_suffix + * The default file cache suffix. + * + * @return string + * The file cache suffix. + */ + protected function getFileCacheSuffix(string $default_suffix): string { + return $default_suffix; + } + + /** + * {@inheritdoc} + */ + public function getDefinitions() { + $definitions = []; + + // Search for classes within all PSR-4 namespace locations. + foreach ($this->getPluginNamespaces() as $namespace => $dirs) { + foreach ($dirs as $dir) { + if (file_exists($dir)) { + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS) + ); + foreach ($iterator as $fileinfo) { + assert($fileinfo instanceof \SplFileInfo); + if ($fileinfo->getExtension() === 'php') { + if ($cached = $this->fileCache->get($fileinfo->getPathName())) { + if (isset($cached['id'])) { + // Explicitly unserialize this to create a new object instance. + $definitions[$cached['id']] = unserialize($cached['content']); + } + continue; + } + + $sub_path = $iterator->getSubIterator()->getSubPath(); + $sub_path = $sub_path ? str_replace(DIRECTORY_SEPARATOR, '\\', $sub_path) . '\\' : ''; + $class = $namespace . '\\' . $sub_path . $fileinfo->getBasename('.php'); + + ['id' => $id, 'content' => $content] = $this->parseClass($class, $fileinfo); + + if ($id) { + $definitions[$id] = $content; + // Explicitly serialize this to create a new object instance. + $this->fileCache->set($fileinfo->getPathName(), ['id' => $id, 'content' => serialize($content)]); + } + else { + // Store a NULL object, so the file is not reparsed again. + $this->fileCache->set($fileinfo->getPathName(), [NULL]); + } + } + } + } + } + } + + // Plugin discovery is a memory expensive process due to reflection and the + // number of files involved. Collect cycles at the end of discovery to be as + // efficient as possible. + gc_collect_cycles(); + return $definitions; + } + + /** + * Parses attributes from a class. + * + * @param class-string $class + * The class to parse. + * @param \SplFileInfo $fileinfo + * The SPL file information for the class. + * + * @return array + * An array with the keys 'id' and 'content'. The 'id' is the plugin ID and + * 'content' is the plugin definition. + * + * @throws \ReflectionException + */ + protected function parseClass(string $class, \SplFileInfo $fileinfo): array { + // @todo Consider performance improvements over using reflection. + // @see https://www.drupal.org/project/drupal/issues/3395260. + $reflection_class = new \ReflectionClass($class); + + $id = $content = NULL; + if ($attributes = $reflection_class->getAttributes($this->pluginDefinitionAttributeName, \ReflectionAttribute::IS_INSTANCEOF)) { + /** @var \Drupal\Component\Plugin\Attribute\AttributeInterface $attribute */ + $attribute = $attributes[0]->newInstance(); + $this->prepareAttributeDefinition($attribute, $class); + + $id = $attribute->getId(); + $content = $attribute->get(); + } + return ['id' => $id, 'content' => $content]; + } + + /** + * Prepares the attribute definition. + * + * @param \Drupal\Component\Plugin\Attribute\AttributeInterface $attribute + * The attribute derived from the plugin. + * @param string $class + * The class used for the plugin. + */ + protected function prepareAttributeDefinition(AttributeInterface $attribute, string $class): void { + $attribute->setClass($class); + } + + /** + * Gets an array of PSR-4 namespaces to search for plugin classes. + * + * @return string[][] + */ + protected function getPluginNamespaces(): array { + return $this->pluginNamespaces; + } + +} diff --git a/core/lib/Drupal/Core/Action/ActionManager.php b/core/lib/Drupal/Core/Action/ActionManager.php index fda253b5a9fb252754cc16af0ffb5d504ef6ba28..39a8c7520b3aa6b9f6d31dabff2e6686a896636f 100644 --- a/core/lib/Drupal/Core/Action/ActionManager.php +++ b/core/lib/Drupal/Core/Action/ActionManager.php @@ -3,6 +3,7 @@ namespace Drupal\Core\Action; use Drupal\Component\Plugin\CategorizingPluginManagerInterface; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\CategorizingPluginManagerTrait; @@ -32,7 +33,7 @@ class ActionManager extends DefaultPluginManager implements CategorizingPluginMa * The module handler to invoke the alter hook with. */ public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { - parent::__construct('Plugin/Action', $namespaces, $module_handler, 'Drupal\Core\Action\ActionInterface', 'Drupal\Core\Annotation\Action'); + parent::__construct('Plugin/Action', $namespaces, $module_handler, 'Drupal\Core\Action\ActionInterface', Action::class, 'Drupal\Core\Annotation\Action'); $this->alterInfo('action_info'); $this->setCacheBackend($cache_backend, 'action_info'); } diff --git a/core/lib/Drupal/Core/Action/Attribute/Action.php b/core/lib/Drupal/Core/Action/Attribute/Action.php new file mode 100644 index 0000000000000000000000000000000000000000..eab816f670075f758149d4212e1d0351a24848ad --- /dev/null +++ b/core/lib/Drupal/Core/Action/Attribute/Action.php @@ -0,0 +1,53 @@ +<?php + +namespace Drupal\Core\Action\Attribute; + +use Drupal\Component\Plugin\Attribute\Plugin; +use Drupal\Core\StringTranslation\TranslatableMarkup; + +/** + * Defines an Action attribute object. + * + * Plugin Namespace: Plugin\Action + * + * @see \Drupal\Core\Action\ActionInterface + * @see \Drupal\Core\Action\ActionManager + * @see \Drupal\Core\Action\ActionBase + * @see \Drupal\Core\Action\Plugin\Action\UnpublishAction + * @see plugin_api + * + * @Annotation + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class Action extends Plugin { + + /** + * Constructs an Action attribute. + * + * @param string $id + * The plugin ID. + * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $label + * The label of the action. + * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $action_label + * (optional) A label that can be used by the action deriver. + * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $category + * (optional) The category under which the action should be listed in the + * UI. + * @param string|null $deriver + * (optional) The deriver class. + * @param string|null $confirm_form_route_name + * (optional) The route name for a confirmation form for this action. + * @param string|null $type + * (optional) The entity type the action can apply to. + */ + public function __construct( + public readonly string $id, + public readonly ?TranslatableMarkup $label = NULL, + public readonly ?TranslatableMarkup $action_label = NULL, + public readonly ?TranslatableMarkup $category = NULL, + public readonly ?string $deriver = NULL, + public readonly ?string $confirm_form_route_name = NULL, + public readonly ?string $type = NULL + ) {} + +} diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php index 28744b6913a1e7aee91588db8fa2f1aae6ccf7c0..6a7994a977abb9ce1156686cb56e68bb98730942 100644 --- a/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php +++ b/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php @@ -2,20 +2,22 @@ namespace Drupal\Core\Action\Plugin\Action; +use Drupal\Core\Action\Plugin\Action\Derivative\EntityDeleteActionDeriver; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\TempStore\PrivateTempStoreFactory; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Redirects to an entity deletion form. - * - * @Action( - * id = "entity:delete_action", - * action_label = @Translation("Delete"), - * deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityDeleteActionDeriver", - * ) */ +#[Action( + id: 'entity:delete_action', + action_label: new TranslatableMarkup('Delete'), + deriver: EntityDeleteActionDeriver::class +)] class DeleteAction extends EntityActionBase { /** diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/EmailAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/EmailAction.php index 78af30dccf9fcb6d1192877a4d53b27c3996b349..e0f6d0896be66f0778e54d456abc21d04ade04e0 100644 --- a/core/lib/Drupal/Core/Action/Plugin/Action/EmailAction.php +++ b/core/lib/Drupal/Core/Action/Plugin/Action/EmailAction.php @@ -6,25 +6,26 @@ use Drupal\Component\Utility\EmailValidatorInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ConfigurableActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Mail\MailManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Utility\Token; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Sends an email message. - * - * @Action( - * id = "action_send_email_action", - * label = @Translation("Send email"), - * type = "system" - * ) */ +#[Action( + id: 'action_send_email_action', + label: new TranslatableMarkup('Send email'), + type: 'system' +)] class EmailAction extends ConfigurableActionBase implements ContainerFactoryPluginInterface { /** diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/GotoAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/GotoAction.php index befc21f6a529fece81bf15dfef331143999eb881..565529af7223a988abaa2794b2347c083b83b5e6 100644 --- a/core/lib/Drupal/Core/Action/Plugin/Action/GotoAction.php +++ b/core/lib/Drupal/Core/Action/Plugin/Action/GotoAction.php @@ -5,9 +5,11 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ConfigurableActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Utility\UnroutedUrlAssemblerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -16,13 +18,12 @@ /** * Redirects to a different URL. - * - * @Action( - * id = "action_goto_action", - * label = @Translation("Redirect to URL"), - * type = "system" - * ) */ +#[Action( + id: 'action_goto_action', + label: new TranslatableMarkup('Redirect to URL'), + type: 'system' +)] class GotoAction extends ConfigurableActionBase implements ContainerFactoryPluginInterface { /** diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/MessageAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/MessageAction.php index 19575278cc177456e5749ec5ba2939ffaa0d2aec..7a9dd005f60db195f5d493632e0967eb54c33800 100644 --- a/core/lib/Drupal/Core/Action/Plugin/Action/MessageAction.php +++ b/core/lib/Drupal/Core/Action/Plugin/Action/MessageAction.php @@ -4,23 +4,24 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ConfigurableActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Utility\Token; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Sends a message to the current user's screen. - * - * @Action( - * id = "action_message_action", - * label = @Translation("Display a message to the user"), - * type = "system" - * ) */ +#[Action( + id: 'action_message_action', + label: new TranslatableMarkup('Display a message to the user'), + type: 'system' +)] class MessageAction extends ConfigurableActionBase implements ContainerFactoryPluginInterface { /** diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php index 8d0cf756b47c308abab7c88e2f2c6bd55754a7ec..0c79430a3f0702de1d7771db8b29dc585bee09f5 100644 --- a/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php +++ b/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php @@ -2,17 +2,19 @@ namespace Drupal\Core\Action\Plugin\Action; +use Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Publishes an entity. - * - * @Action( - * id = "entity:publish_action", - * action_label = @Translation("Publish"), - * deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver", - * ) */ +#[Action( + id: 'entity:publish_action', + action_label: new TranslatableMarkup('Publish'), + deriver: EntityPublishedActionDeriver::class +)] class PublishAction extends EntityActionBase { /** diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php index fc63e5c16e3bd16a1597547c7aed0295b82884c4..e9fbbd62e728aa435e405361c6f1d06212634a07 100644 --- a/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php +++ b/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php @@ -3,19 +3,21 @@ namespace Drupal\Core\Action\Plugin\Action; use Drupal\Component\Datetime\TimeInterface; +use Drupal\Core\Action\Plugin\Action\Derivative\EntityChangedActionDeriver; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides an action that can save any entity. - * - * @Action( - * id = "entity:save_action", - * action_label = @Translation("Save"), - * deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityChangedActionDeriver", - * ) */ +#[Action( + id: 'entity:save_action', + action_label: new TranslatableMarkup('Save'), + deriver: EntityChangedActionDeriver::class +)] class SaveAction extends EntityActionBase { /** diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php index bb3f6c2d98e772124c2598cb7f0dfdc2d787a967..b8ef0f3db66ebde5721355b44605bf4ea21b44f6 100644 --- a/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php +++ b/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php @@ -2,17 +2,19 @@ namespace Drupal\Core\Action\Plugin\Action; +use Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Unpublishes an entity. - * - * @Action( - * id = "entity:unpublish_action", - * action_label = @Translation("Unpublish"), - * deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver", - * ) */ +#[Action( + id: 'entity:unpublish_action', + action_label: new TranslatableMarkup('Unpublish'), + deriver: EntityPublishedActionDeriver::class +)] class UnpublishAction extends EntityActionBase { /** diff --git a/core/lib/Drupal/Core/Block/Attribute/Block.php b/core/lib/Drupal/Core/Block/Attribute/Block.php new file mode 100644 index 0000000000000000000000000000000000000000..3e5fbc6be88c76107e31755d2e330c89372f0b57 --- /dev/null +++ b/core/lib/Drupal/Core/Block/Attribute/Block.php @@ -0,0 +1,40 @@ +<?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 = [] + ) {} + +} diff --git a/core/lib/Drupal/Core/Block/BlockManager.php b/core/lib/Drupal/Core/Block/BlockManager.php index 026d810fc058b3330376d67532d94fd42abd7559..0741e1579403205f70e799d0b2bf0dae31029b25 100644 --- a/core/lib/Drupal/Core/Block/BlockManager.php +++ b/core/lib/Drupal/Core/Block/BlockManager.php @@ -3,6 +3,7 @@ namespace Drupal\Core\Block; use Drupal\Component\Plugin\FallbackPluginManagerInterface; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\CategorizingPluginManagerTrait; @@ -45,7 +46,7 @@ class BlockManager extends DefaultPluginManager implements BlockManagerInterface * The logger. */ 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'); + parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\Core\Block\BlockPluginInterface', Block::class, 'Drupal\Core\Block\Annotation\Block'); $this->alterInfo($this->getType()); $this->setCacheBackend($cache_backend, 'block_plugins'); diff --git a/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php b/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php index 0d09afb85044003f42b73a0d268ec716c22eda58..2d92c161d94db0c52171c9bafa5fb5923e78ab06 100644 --- a/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php +++ b/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Block\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Block\BlockPluginTrait; use Drupal\Core\Cache\CacheableDependencyTrait; @@ -9,17 +10,17 @@ use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines a fallback plugin for missing block plugins. - * - * @Block( - * id = "broken", - * admin_label = @Translation("Broken/Missing"), - * category = @Translation("Block"), - * ) */ +#[Block( + id: "broken", + admin_label: new TranslatableMarkup("Broken/Missing"), + category: new TranslatableMarkup("Block") +)] class Broken extends PluginBase implements BlockPluginInterface, ContainerFactoryPluginInterface { use BlockPluginTrait; diff --git a/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php b/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php index af1fb6ee374be3714f64dcf23485365070be64ec..2689acae4a3a89a94f7e7d43ad51ea13ef7e147b 100644 --- a/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php +++ b/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php @@ -2,20 +2,21 @@ namespace Drupal\Core\Block\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Block\TitleBlockPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a block to display the page title. - * - * @Block( - * id = "page_title_block", - * admin_label = @Translation("Page title"), - * forms = { - * "settings_tray" = FALSE, - * }, - * ) */ +#[Block( + id: "page_title_block", + admin_label: new TranslatableMarkup("Page title"), + forms: [ + 'settings_tray' => FALSE, + ] +)] class PageTitleBlock extends BlockBase implements TitleBlockPluginInterface { /** diff --git a/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php b/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php index 70ef668bf8590b01df16972b7297602eff7cf4b8..27591d4a3f39747097011d838aa107226cd28f78 100644 --- a/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php +++ b/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php @@ -2,20 +2,21 @@ namespace Drupal\Core\Menu\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Menu\LocalActionManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Routing\RouteMatchInterface; /** * Provides a block to display the local actions. - * - * @Block( - * id = "local_actions_block", - * admin_label = @Translation("Primary admin actions") - * ) */ +#[Block( + id: "local_actions_block", + admin_label: new TranslatableMarkup("Primary admin actions") +)] class LocalActionsBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/lib/Drupal/Core/Menu/Plugin/Block/LocalTasksBlock.php b/core/lib/Drupal/Core/Menu/Plugin/Block/LocalTasksBlock.php index 6a518ef3eff0514365f6abf5272a8cfe9d3e4153..0d7cf1c3a2d796565794ec2dad1884254e7bdff6 100644 --- a/core/lib/Drupal/Core/Menu/Plugin/Block/LocalTasksBlock.php +++ b/core/lib/Drupal/Core/Menu/Plugin/Block/LocalTasksBlock.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Menu\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Form\FormStateInterface; @@ -9,16 +10,16 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a "Tabs" block to display the local tasks. - * - * @Block( - * id = "local_tasks_block", - * admin_label = @Translation("Tabs"), - * ) */ +#[Block( + id: "local_tasks_block", + admin_label: new TranslatableMarkup("Tabs") +)] class LocalTasksBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php b/core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php index bfc234b958e6107d5751e396fc6898f38005935d..e68a4c0a40718c8dac914f27acb0eb33bc4c64ed 100644 --- a/core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php +++ b/core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php @@ -104,14 +104,20 @@ public static function create($data_type = 'any') { * The description of this context definition for the UI. * @param mixed $default_value * The default value of this definition. + * @param array $constraints + * An array of constraints keyed by the constraint name and a value of an + * array constraint options or a NULL. */ - public function __construct($data_type = 'any', $label = NULL, $required = TRUE, $multiple = FALSE, $description = NULL, $default_value = NULL) { + public function __construct($data_type = 'any', $label = NULL, $required = TRUE, $multiple = FALSE, $description = NULL, $default_value = NULL, array $constraints = []) { $this->dataType = $data_type; $this->label = $label; $this->isRequired = $required; $this->isMultiple = $multiple; $this->description = $description; $this->defaultValue = $default_value; + foreach ($constraints as $constraint_name => $options) { + $this->addConstraint($constraint_name, $options); + } assert(!str_starts_with($data_type, 'entity:') || $this instanceof EntityContextDefinition); } diff --git a/core/lib/Drupal/Core/Plugin/Context/EntityContextDefinition.php b/core/lib/Drupal/Core/Plugin/Context/EntityContextDefinition.php index abdcf05116f37e495a6bf40ec8770481ddaad8f4..dfb8d0019bf06fb5d5880af6ac71c2829ace743f 100644 --- a/core/lib/Drupal/Core/Plugin/Context/EntityContextDefinition.php +++ b/core/lib/Drupal/Core/Plugin/Context/EntityContextDefinition.php @@ -17,13 +17,13 @@ class EntityContextDefinition extends ContextDefinition { /** * {@inheritdoc} */ - public function __construct($data_type = 'any', $label = NULL, $required = TRUE, $multiple = FALSE, $description = NULL, $default_value = NULL) { + public function __construct($data_type = 'any', $label = NULL, $required = TRUE, $multiple = FALSE, $description = NULL, $default_value = NULL, array $constraints = []) { // Prefix the data type with 'entity:' so that this class can be constructed // like so: new EntityContextDefinition('node') if (!str_starts_with($data_type, 'entity:')) { $data_type = "entity:$data_type"; } - parent::__construct($data_type, $label, $required, $multiple, $description, $default_value); + parent::__construct($data_type, $label, $required, $multiple, $description, $default_value, $constraints); } /** diff --git a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php index 1a4a7daee1207476e777edf35f07b6c40c644c29..fd3d280ff700cf44c56db008a5f6e0e7df142634 100644 --- a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php +++ b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php @@ -3,12 +3,15 @@ namespace Drupal\Core\Plugin; use Drupal\Component\Assertion\Inspector; +use Drupal\Component\Plugin\Attribute\AttributeInterface; use Drupal\Component\Plugin\Definition\PluginDefinitionInterface; use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface; use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\UseCacheBackendTrait; use Drupal\Component\Plugin\Discovery\DiscoveryCachedTrait; +use Drupal\Core\Plugin\Discovery\AttributeClassDiscovery; +use Drupal\Core\Plugin\Discovery\AttributeDiscoveryWithAnnotations; use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator; use Drupal\Component\Plugin\PluginManagerBase; use Drupal\Component\Plugin\PluginManagerInterface; @@ -82,6 +85,13 @@ class DefaultPluginManager extends PluginManagerBase implements PluginManagerInt */ protected $pluginDefinitionAnnotationName; + /** + * The name of the attribute that contains the plugin definition. + * + * @var string + */ + protected $pluginDefinitionAttributeName; + /** * The interface each plugin should implement. * @@ -121,19 +131,33 @@ class DefaultPluginManager extends PluginManagerBase implements PluginManagerInt * The module handler. * @param string|null $plugin_interface * (optional) The interface each plugin should implement. - * @param string $plugin_definition_annotation_name + * @param string|null $plugin_definition_attribute_name + * (optional) The name of the attribute that contains the plugin definition. + * @param string|array|null $plugin_definition_annotation_name * (optional) The name of the annotation that contains the plugin definition. * Defaults to 'Drupal\Component\Annotation\Plugin'. * @param string[] $additional_annotation_namespaces * (optional) Additional namespaces to scan for annotation definitions. + * + * @todo $plugin_definition_attribute_name should default to + * 'Drupal\Component\Plugin\Attribute\Plugin' once annotations are no longer + * supported. */ - public function __construct($subdir, \Traversable $namespaces, ModuleHandlerInterface $module_handler, $plugin_interface = NULL, $plugin_definition_annotation_name = 'Drupal\Component\Annotation\Plugin', array $additional_annotation_namespaces = []) { + public function __construct($subdir, \Traversable $namespaces, ModuleHandlerInterface $module_handler, $plugin_interface = NULL, ?string $plugin_definition_attribute_name = NULL, string|array $plugin_definition_annotation_name = NULL, array $additional_annotation_namespaces = []) { $this->subdir = $subdir; $this->namespaces = $namespaces; - $this->pluginDefinitionAnnotationName = $plugin_definition_annotation_name; - $this->pluginInterface = $plugin_interface; $this->moduleHandler = $module_handler; - $this->additionalAnnotationNamespaces = $additional_annotation_namespaces; + $this->pluginInterface = $plugin_interface; + if (is_subclass_of($plugin_definition_attribute_name, AttributeInterface::class)) { + $this->pluginDefinitionAttributeName = $plugin_definition_attribute_name; + $this->pluginDefinitionAnnotationName = $plugin_definition_annotation_name; + $this->additionalAnnotationNamespaces = $additional_annotation_namespaces; + } + else { + // Backward compatibility. + $this->pluginDefinitionAnnotationName = $plugin_definition_attribute_name ?? 'Drupal\Component\Annotation\Plugin'; + $this->additionalAnnotationNamespaces = $plugin_definition_annotation_name ?? []; + } } /** @@ -265,7 +289,15 @@ public function processDefinition(&$definition, $plugin_id) { */ protected function getDiscovery() { if (!$this->discovery) { - $discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces); + if (isset($this->pluginDefinitionAttributeName) && isset($this->pluginDefinitionAnnotationName)) { + $discovery = new AttributeDiscoveryWithAnnotations($this->subdir, $this->namespaces, $this->pluginDefinitionAttributeName, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces); + } + elseif (isset($this->pluginDefinitionAttributeName)) { + $discovery = new AttributeClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAttributeName); + } + else { + $discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces); + } $this->discovery = new ContainerDerivativeDiscoveryDecorator($discovery); } return $this->discovery; diff --git a/core/lib/Drupal/Core/Plugin/Discovery/AttributeClassDiscovery.php b/core/lib/Drupal/Core/Plugin/Discovery/AttributeClassDiscovery.php new file mode 100644 index 0000000000000000000000000000000000000000..38b0ff715b034e6ecefd4390c81564024a227890 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Discovery/AttributeClassDiscovery.php @@ -0,0 +1,124 @@ +<?php + +namespace Drupal\Core\Plugin\Discovery; + +use Drupal\Component\Plugin\Attribute\AttributeInterface; +use Drupal\Component\Plugin\Discovery\AttributeClassDiscovery as ComponentAttributeClassDiscovery; + +/** + * Defines a discovery mechanism to find plugins using attributes. + */ +class AttributeClassDiscovery extends ComponentAttributeClassDiscovery { + + /** + * A suffix to append to each PSR-4 directory associated with a base namespace. + * + * This suffix is used to form the directories where plugins are found. + * + * @var string + */ + protected $directorySuffix = ''; + + /** + * A suffix to append to each base namespace. + * + * This suffix is used to obtain the namespaces where plugins are found. + * + * @var string + */ + protected $namespaceSuffix = ''; + + /** + * Constructs an AttributeClassDiscovery object. + * + * @param string $subdir + * Either the plugin's subdirectory, for example 'Plugin/views/filter', or + * empty string if plugins are located at the top level of the namespace. + * @param \Traversable $rootNamespacesIterator + * An object that implements \Traversable which contains the root paths + * keyed by the corresponding namespace to look for plugin implementations. + * If $subdir is not an empty string, it will be appended to each namespace. + * @param string $pluginDefinitionAttributeName + * (optional) The name of the attribute that contains the plugin definition. + * Defaults to 'Drupal\Component\Plugin\Attribute\Plugin'. + */ + public function __construct( + string $subdir, + protected \Traversable $rootNamespacesIterator, + string $pluginDefinitionAttributeName = 'Drupal\Component\Plugin\Attribute\Plugin', + ) { + if ($subdir) { + // Prepend a directory separator to $subdir, + // if it does not already have one. + if ('/' !== $subdir[0]) { + $subdir = '/' . $subdir; + } + $this->directorySuffix = $subdir; + $this->namespaceSuffix = str_replace('/', '\\', $subdir); + } + parent::__construct([], $pluginDefinitionAttributeName); + } + + /** + * {@inheritdoc} + */ + protected function prepareAttributeDefinition(AttributeInterface $attribute, string $class): void { + parent::prepareAttributeDefinition($attribute, $class); + + if (!$attribute->getProvider()) { + $attribute->setProvider($this->getProviderFromNamespace($class)); + } + } + + /** + * Extracts the provider name from a Drupal namespace. + * + * @param string $namespace + * The namespace to extract the provider from. + * + * @return string|null + * The matching provider name, or NULL otherwise. + */ + protected function getProviderFromNamespace(string $namespace): ?string { + preg_match('|^Drupal\\\\(?<provider>[\w]+)\\\\|', $namespace, $matches); + + if (isset($matches['provider'])) { + return mb_strtolower($matches['provider']); + } + + return NULL; + } + + /** + * {@inheritdoc} + */ + protected function getPluginNamespaces(): array { + $plugin_namespaces = []; + if ($this->namespaceSuffix) { + foreach ($this->rootNamespacesIterator as $namespace => $dirs) { + // Append the namespace suffix to the base namespace, to obtain the + // plugin namespace; for example, 'Drupal\views' may become + // 'Drupal\views\Plugin\Block'. + $namespace .= $this->namespaceSuffix; + foreach ((array) $dirs as $dir) { + // Append the directory suffix to the PSR-4 base directory, to obtain + // the directory where plugins are found. For example, + // DRUPAL_ROOT . '/core/modules/views/src' may become + // DRUPAL_ROOT . '/core/modules/views/src/Plugin/Block'. + $plugin_namespaces[$namespace][] = $dir . $this->directorySuffix; + } + } + } + else { + // Both the namespace suffix and the directory suffix are empty, + // so the plugin namespaces and directories are the same as the base + // directories. + foreach ($this->rootNamespacesIterator as $namespace => $dirs) { + $plugin_namespaces[$namespace] = (array) $dirs; + } + } + + return $plugin_namespaces; + } + +} diff --git a/core/lib/Drupal/Core/Plugin/Discovery/AttributeDiscoveryWithAnnotations.php b/core/lib/Drupal/Core/Plugin/Discovery/AttributeDiscoveryWithAnnotations.php new file mode 100644 index 0000000000000000000000000000000000000000..f0ddae339329b9eab1b9bbe709c524003e7c3fc2 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Discovery/AttributeDiscoveryWithAnnotations.php @@ -0,0 +1,150 @@ +<?php + +namespace Drupal\Core\Plugin\Discovery; + +use Doctrine\Common\Annotations\AnnotationRegistry; +use Drupal\Component\Annotation\AnnotationInterface; +use Drupal\Component\Annotation\Doctrine\SimpleAnnotationReader; +use Drupal\Component\Annotation\Doctrine\StaticReflectionParser; +use Drupal\Component\Annotation\Reflection\MockFileFinder; +use Drupal\Component\Utility\Crypt; + +/** + * Enables both attribute and annotation discovery for plugin definitions. + */ +class AttributeDiscoveryWithAnnotations extends AttributeClassDiscovery { + + /** + * The doctrine annotation reader. + * + * @var \Doctrine\Common\Annotations\Reader + */ + protected $annotationReader; + + /** + * Constructs an AttributeDiscoveryWithAnnotations object. + * + * @param string $subdir + * Either the plugin's subdirectory, for example 'Plugin/views/filter', or + * empty string if plugins are located at the top level of the namespace. + * @param \Traversable $rootNamespaces + * An object that implements \Traversable which contains the root paths + * keyed by the corresponding namespace to look for plugin implementations. + * If $subdir is not an empty string, it will be appended to each namespace. + * @param string $pluginDefinitionAttributeName + * (optional) The name of the attribute that contains the plugin definition. + * Defaults to 'Drupal\Component\Plugin\Attribute\Plugin'. + * @param string $pluginDefinitionAnnotationName + * (optional) The name of the attribute that contains the plugin definition. + * Defaults to 'Drupal\Component\Annotation\Plugin'. + * @param string[] $additionalNamespaces + * (optional) Additional namespaces to scan for attribute definitions. + */ + public function __construct( + string $subdir, + \Traversable $rootNamespaces, + string $pluginDefinitionAttributeName = 'Drupal\Component\Plugin\Attribute\Plugin', + protected readonly string $pluginDefinitionAnnotationName = 'Drupal\Component\Annotation\Plugin', + protected readonly array $additionalNamespaces = [], + ) { + parent::__construct($subdir, $rootNamespaces, $pluginDefinitionAttributeName); + } + + /** + * {@inheritdoc} + */ + protected function getFileCacheSuffix(string $default_suffix):string { + return $default_suffix . ':' . Crypt::hashBase64(serialize($this->additionalNamespaces)) . ':' . str_replace('\\', '_', $this->pluginDefinitionAnnotationName); + } + + /** + * {@inheritdoc} + */ + public function getDefinitions() { + // Clear the annotation loaders of any previous annotation classes. + AnnotationRegistry::reset(); + // Register the namespaces of classes that can be used for annotations. + // @phpstan-ignore-next-line + AnnotationRegistry::registerLoader('class_exists'); + + $definitions = parent::getDefinitions(); + + $this->annotationReader = NULL; + + return $definitions; + } + + /** + * {@inheritdoc} + */ + protected function parseClass(string $class, \SplFileInfo $fileinfo): array { + // The filename is already known, so there is no need to find the + // file. However, StaticReflectionParser needs a finder, so use a + // mock version. + $finder = MockFileFinder::create($fileinfo->getPathName()); + $parser = new StaticReflectionParser($class, $finder, TRUE); + + // @todo Handle deprecating definitions discovery via annotations in + // https://www.drupal.org/project/drupal/issues/3265945. + /** @var \Drupal\Component\Annotation\AnnotationInterface $annotation */ + if ($annotation = $this->getAnnotationReader()->getClassAnnotation($parser->getReflectionClass(), $this->pluginDefinitionAnnotationName)) { + $this->prepareAnnotationDefinition($annotation, $class); + return ['id' => $annotation->getId(), 'content' => $annotation->get()]; + } + + return parent::parseClass($class, $fileinfo); + } + + /** + * Prepares the annotation definition. + * + * This is a copy of the prepareAnnotationDefinition method from annotated + * class discovery. + * + * @param \Drupal\Component\Annotation\AnnotationInterface $annotation + * The annotation derived from the plugin. + * @param class-string $class + * The class used for the plugin. + * + * @see \Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery::prepareAnnotationDefinition() + * @see \Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery::prepareAnnotationDefinition() + */ + private function prepareAnnotationDefinition(AnnotationInterface $annotation, string $class): void { + $annotation->setClass($class); + if (!$annotation->getProvider()) { + $annotation->setProvider($this->getProviderFromNamespace($class)); + } + } + + /** + * Gets the used doctrine annotation reader. + * + * This is a copy of the getAnnotationReader method from annotated class + * discovery. + * + * @return \Drupal\Component\Annotation\Doctrine\SimpleAnnotationReader + * The annotation reader. + * + * @see \Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery::getAnnotationReader() + * @see \Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery::getAnnotationReader() + */ + private function getAnnotationReader() : SimpleAnnotationReader { + if (!isset($this->annotationReader)) { + $this->annotationReader = new SimpleAnnotationReader(); + + // Add the namespaces from the main plugin annotation, like @EntityType. + $namespace = substr($this->pluginDefinitionAnnotationName, 0, strrpos($this->pluginDefinitionAnnotationName, '\\')); + $this->annotationReader->addNamespace($namespace); + + // Register additional namespaces to be scanned for annotations. + foreach ($this->additionalNamespaces as $namespace) { + $this->annotationReader->addNamespace($namespace); + } + + // Add the Core annotation classes like @Translation. + $this->annotationReader->addNamespace('Drupal\Core\Annotation'); + } + return $this->annotationReader; + } + +} diff --git a/core/modules/action/tests/action_form_ajax_test/src/Plugin/Action/ActionAjaxTest.php b/core/modules/action/tests/action_form_ajax_test/src/Plugin/Action/ActionAjaxTest.php index 8afc98cf54110cf1646e16c1d07fdeb59a0b8822..827438c69645949888f008bac275610c69a79f9d 100644 --- a/core/modules/action/tests/action_form_ajax_test/src/Plugin/Action/ActionAjaxTest.php +++ b/core/modules/action/tests/action_form_ajax_test/src/Plugin/Action/ActionAjaxTest.php @@ -4,18 +4,19 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ConfigurableActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Plugin used for testing AJAX in action config entity forms. - * - * @Action( - * id = "action_form_ajax_test", - * label = @Translation("action_form_ajax_test"), - * type = "system" - * ) */ +#[Action( + id: 'action_form_ajax_test', + label: new TranslatableMarkup('action_form_ajax_test'), + type: 'system' +)] class ActionAjaxTest extends ConfigurableActionBase { /** diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php index 4bb9f24cc0fd00811dd9261b36a89f78d87255f9..f663f701f2c4a4f20485a6f789164c0f7c25c13c 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php @@ -3,21 +3,22 @@ namespace Drupal\block_test\Plugin\Block; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\Cache; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\State\StateInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a block to test access. - * - * @Block( - * id = "test_access", - * admin_label = @Translation("Test block access") - * ) */ +#[Block( + id: "test_access", + admin_label: new TranslatableMarkup("Test block access"), +)] class TestAccessBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestBlockInstantiation.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestBlockInstantiation.php index 0d146390c518573757ec71bb3f1f0dfaa030e607..77df48e8d995ed3252d031097f79339c38b9133a 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestBlockInstantiation.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestBlockInstantiation.php @@ -3,18 +3,19 @@ namespace Drupal\block_test\Plugin\Block; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a basic block for testing block instantiation and configuration. - * - * @Block( - * id = "test_block_instantiation", - * admin_label = @Translation("Display message") - * ) */ +#[Block( + id: "test_block_instantiation", + admin_label: new TranslatableMarkup("Display message") +)] class TestBlockInstantiation extends BlockBase { /** diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestCacheBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestCacheBlock.php index 5fec14ccc7719f1c0045651bb1717531bb5b5aa8..dc27b08ef09c44838b75ae37decabbb16c51d0f4 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestCacheBlock.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestCacheBlock.php @@ -2,16 +2,17 @@ namespace Drupal\block_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a block to test caching. - * - * @Block( - * id = "test_cache", - * admin_label = @Translation("Test block caching") - * ) */ +#[Block( + id: "test_cache", + admin_label: new TranslatableMarkup("Test block caching") +)] class TestCacheBlock extends BlockBase { /** diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php index e1df7485107df1ae15b8f5645f66e6f832b422b8..fd78dafdc8ffcb5683483085de9350cafb22cb9d 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php @@ -2,23 +2,30 @@ namespace Drupal\block_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\Plugin\Context\EntityContextDefinition; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\user\UserInterface; /** * Provides a context-aware block. - * - * @Block( - * id = "test_context_aware", - * admin_label = @Translation("Test context-aware block"), - * context_definitions = { - * "user" = @ContextDefinition("entity:user", required = FALSE, - * label = @Translation("User Context"), constraints = { "NotNull" = {} } - * ), - * } - * ) */ +#[Block( + id: "test_context_aware", + admin_label: new TranslatableMarkup("Test context-aware block"), + context_definitions: [ + 'user' => new EntityContextDefinition( + data_type: 'entity:user', + label: new TranslatableMarkup("User Context"), + required: FALSE, + constraints: [ + "NotNull" => [], + ] + ), + ] +)] class TestContextAwareBlock extends BlockBase { /** diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareNoValidContextOptionsBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareNoValidContextOptionsBlock.php index 234bf9a3015d763b17699ba432a0bd51c0f47b2d..31944bed464143833d2559ccb4bd3b999ac4993e 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareNoValidContextOptionsBlock.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareNoValidContextOptionsBlock.php @@ -2,19 +2,21 @@ namespace Drupal\block_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a context-aware block that uses a not-passed, non-required context. - * - * @Block( - * id = "test_context_aware_no_valid_context_options", - * admin_label = @Translation("Test context-aware block - no valid context options"), - * context_definitions = { - * "email" = @ContextDefinition("email", required = FALSE) - * } - * ) */ +#[Block( + id: "test_context_aware_no_valid_context_options", + admin_label: new TranslatableMarkup("Test context-aware block - no valid context options"), + context_definitions: [ + 'user' => new ContextDefinition(data_type: 'email', required: FALSE), + ] +)] class TestContextAwareNoValidContextOptionsBlock extends BlockBase { /** diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareUnsatisfiedBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareUnsatisfiedBlock.php index d0789e37a7d1d12b48bda1dc70a861273cdb93e5..74dc183794561c358d9f653a822693065f89a19c 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareUnsatisfiedBlock.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareUnsatisfiedBlock.php @@ -2,19 +2,21 @@ namespace Drupal\block_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\Plugin\Context\EntityContextDefinition; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a context-aware block. - * - * @Block( - * id = "test_context_aware_unsatisfied", - * admin_label = @Translation("Test context-aware unsatisfied block"), - * context_definitions = { - * "user" = @ContextDefinition("entity:foobar") - * } - * ) */ +#[Block( + id: "test_context_aware_unsatisfied", + admin_label: new TranslatableMarkup("Test context-aware unsatisfied block"), + context_definitions: [ + 'user' => new EntityContextDefinition('entity:foobar'), + ] +)] class TestContextAwareUnsatisfiedBlock extends BlockBase { /** diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestFormBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestFormBlock.php index e499fa88c90e035e6f27f4836254d565e637e75d..c4eef85d0ee01d538d0a7d8a00236ea69b75e32f 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestFormBlock.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestFormBlock.php @@ -2,16 +2,17 @@ namespace Drupal\block_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a block to test caching. - * - * @Block( - * id = "test_form_in_block", - * admin_label = @Translation("Test form block caching") - * ) */ +#[Block( + id: "test_form_in_block", + admin_label: new TranslatableMarkup("Test form block caching"), +)] class TestFormBlock extends BlockBase { /** diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestHtmlBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestHtmlBlock.php index 35f08c4b041cd6a471d85fd864be67ac0206c82f..5644c3b5cc1416f543e0d44a09abfa4a8ed3ed12 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestHtmlBlock.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestHtmlBlock.php @@ -2,16 +2,17 @@ namespace Drupal\block_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a block to test HTML. - * - * @Block( - * id = "test_html", - * admin_label = @Translation("Test HTML block") - * ) */ +#[Block( + id: "test_html", + admin_label: new TranslatableMarkup("Test HTML block"), +)] class TestHtmlBlock extends BlockBase { /** diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php index 64b70b22c04ef34b8d520495c9e7719d07e9eb8a..f227f337b6dadd057587d49b604af9bfaf23369d 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php @@ -2,19 +2,21 @@ namespace Drupal\block_test\Plugin\Block; +use Drupal\block_test\PluginForm\EmptyBlockForm; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a block with multiple forms. - * - * @Block( - * id = "test_multiple_forms_block", - * forms = { - * "secondary" = "\Drupal\block_test\PluginForm\EmptyBlockForm" - * }, - * admin_label = @Translation("Multiple forms test block") - * ) */ +#[Block( + id: "test_multiple_forms_block", + forms: [ + 'secondary' => EmptyBlockForm::class, + ], + admin_label: new TranslatableMarkup("Multiple forms test block"), +)] class TestMultipleFormsBlock extends BlockBase { /** diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestSettingsValidationBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestSettingsValidationBlock.php index d1f16d7fda40c1e9d981b8a6eb2348a9e18c0886..cad100b00734cf55a476db32d2bb4cb1e74bcc3c 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestSettingsValidationBlock.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestSettingsValidationBlock.php @@ -2,17 +2,18 @@ namespace Drupal\block_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a test settings validation block. - * - * @Block( - * id = "test_settings_validation", - * admin_label = @Translation("Test settings validation block"), - * ) */ +#[Block( + id: "test_settings_validation", + admin_label: new TranslatableMarkup("Test settings validation block"), +)] class TestSettingsValidationBlock extends BlockBase { /** diff --git a/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php b/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php index 1b2a9f19a161e3129443d7e7c7018bd6ea6c4f75..7c862f499fd418c8047d14f7f8a815c03b8c9f33 100644 --- a/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php +++ b/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php @@ -3,7 +3,9 @@ namespace Drupal\block_content\Plugin\Block; use Drupal\block_content\BlockContentUuidLookup; +use Drupal\block_content\Plugin\Derivative\BlockContent; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Block\BlockManagerInterface; use Drupal\Core\Entity\EntityDisplayRepositoryInterface; @@ -12,18 +14,18 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines a generic block type. - * - * @Block( - * id = "block_content", - * admin_label = @Translation("Content block"), - * category = @Translation("Content block"), - * deriver = "Drupal\block_content\Plugin\Derivative\BlockContent" - * ) */ +#[Block( + id: "block_content", + admin_label: new TranslatableMarkup("Content block"), + category: new TranslatableMarkup("Content block"), + deriver: BlockContent::class +)] class BlockContentBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/book/src/Plugin/Block/BookNavigationBlock.php b/core/modules/book/src/Plugin/Block/BookNavigationBlock.php index 86d957739f038ceca24ee55f724615cf14ed4602..578ea3655d0d4ee9cc24abe3fbd37670297df855 100644 --- a/core/modules/book/src/Plugin/Block/BookNavigationBlock.php +++ b/core/modules/book/src/Plugin/Block/BookNavigationBlock.php @@ -2,11 +2,13 @@ namespace Drupal\book\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\book\BookManagerInterface; use Drupal\Core\Cache\Cache; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\node\NodeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Routing\RouteMatchInterface; @@ -14,13 +16,12 @@ /** * Provides a 'Book navigation' block. - * - * @Block( - * id = "book_navigation", - * admin_label = @Translation("Book navigation"), - * category = @Translation("Menus") - * ) */ +#[Block( + id: "book_navigation", + admin_label: new TranslatableMarkup("Book navigation"), + category: new TranslatableMarkup("Menus") +)] class BookNavigationBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php b/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php index 054196509900f2433a4cfbda9604b42a558f4147..891ae25631445d297bc9ab1051bab7c10ece8f10 100644 --- a/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php +++ b/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php @@ -4,22 +4,23 @@ use Drupal\Component\Utility\Tags; use Drupal\Core\Action\ConfigurableActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Entity\EntityViewBuilderInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Unpublishes a comment containing certain keywords. - * - * @Action( - * id = "comment_unpublish_by_keyword_action", - * label = @Translation("Unpublish comment containing keyword(s)"), - * type = "comment" - * ) */ +#[Action( + id: 'comment_unpublish_by_keyword_action', + label: new TranslatableMarkup('Unpublish comment containing keyword(s)'), + type: 'comment' +)] class UnpublishByKeywordComment extends ConfigurableActionBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/editor/tests/src/Functional/EditorDialogAccessTest.php b/core/modules/editor/tests/src/Functional/EditorDialogAccessTest.php index f12dca0bbf49f020bfa502030261daffaead48bd..dc8b8d7a6588a59f61072ec73c5340b5d0262d9a 100644 --- a/core/modules/editor/tests/src/Functional/EditorDialogAccessTest.php +++ b/core/modules/editor/tests/src/Functional/EditorDialogAccessTest.php @@ -18,7 +18,7 @@ class EditorDialogAccessTest extends BrowserTestBase { * * @var array */ - protected static $modules = ['editor', 'filter', 'editor_test']; + protected static $modules = ['editor', 'filter', 'text', 'editor_test']; /** * {@inheritdoc} diff --git a/core/modules/forum/src/Plugin/Block/ActiveTopicsBlock.php b/core/modules/forum/src/Plugin/Block/ActiveTopicsBlock.php index 921e7ed04b685d0179515c1c3d60de334e2763ba..c4b4bf0ef212e36abbaca23560623001a42a3dde 100644 --- a/core/modules/forum/src/Plugin/Block/ActiveTopicsBlock.php +++ b/core/modules/forum/src/Plugin/Block/ActiveTopicsBlock.php @@ -2,17 +2,18 @@ namespace Drupal\forum\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Database\Database; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides an 'Active forum topics' block. - * - * @Block( - * id = "forum_active_block", - * admin_label = @Translation("Active forum topics"), - * category = @Translation("Lists (Views)") - * ) */ +#[Block( + id: "forum_active_block", + admin_label: new TranslatableMarkup("Active forum topics"), + category: new TranslatableMarkup("Lists (Views)") +)] class ActiveTopicsBlock extends ForumBlockBase { /** diff --git a/core/modules/forum/src/Plugin/Block/NewTopicsBlock.php b/core/modules/forum/src/Plugin/Block/NewTopicsBlock.php index e1d2d1c1afbb44315c85bd3a446b2398590e4817..13d3bf0dc03ed3aa0d0c719f514e1e87e77a71dd 100644 --- a/core/modules/forum/src/Plugin/Block/NewTopicsBlock.php +++ b/core/modules/forum/src/Plugin/Block/NewTopicsBlock.php @@ -2,17 +2,18 @@ namespace Drupal\forum\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Database\Database; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a 'New forum topics' block. - * - * @Block( - * id = "forum_new_block", - * admin_label = @Translation("New forum topics"), - * category = @Translation("Lists (Views)") - * ) */ +#[Block( + id: "forum_new_block", + admin_label: new TranslatableMarkup("New forum topics"), + category: new TranslatableMarkup("Lists (Views)") +)] class NewTopicsBlock extends ForumBlockBase { /** diff --git a/core/modules/help/src/Plugin/Block/HelpBlock.php b/core/modules/help/src/Plugin/Block/HelpBlock.php index 24cd3f3c505743696dba66b71ad3004f6ec427cb..8a593842a231e8073a1341d641f825bf4843aaf8 100644 --- a/core/modules/help/src/Plugin/Block/HelpBlock.php +++ b/core/modules/help/src/Plugin/Block/HelpBlock.php @@ -2,25 +2,24 @@ namespace Drupal\help\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\Cache; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; /** * Provides a 'Help' block. - * - * @Block( - * id = "help_block", - * admin_label = @Translation("Help"), - * forms = { - * "settings_tray" = FALSE, - * }, - * ) */ +#[Block( + id: "help_block", + admin_label: new TranslatableMarkup("Help"), + forms: ['settings_tray' => FALSE] +)] class HelpBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/language/src/Plugin/Block/LanguageBlock.php b/core/modules/language/src/Plugin/Block/LanguageBlock.php index bc64f05b1db1f6ea1aae5c73619f6028a91afa9a..e672ce6583d785ea02753c74e8b995b1ca9e7f2e 100644 --- a/core/modules/language/src/Plugin/Block/LanguageBlock.php +++ b/core/modules/language/src/Plugin/Block/LanguageBlock.php @@ -3,24 +3,26 @@ namespace Drupal\language\Plugin\Block; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Path\PathMatcherInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\language\Plugin\Derivative\LanguageBlock as LanguageBlockDeriver; /** * Provides a 'Language switcher' block. - * - * @Block( - * id = "language_block", - * admin_label = @Translation("Language switcher"), - * category = @Translation("System"), - * deriver = "Drupal\language\Plugin\Derivative\LanguageBlock" - * ) */ +#[Block( + id: "language_block", + admin_label: new TranslatableMarkup("Language switcher"), + category: new TranslatableMarkup("System"), + deriver: LanguageBlockDeriver::class +)] class LanguageBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/layout_builder/src/Plugin/Block/ExtraFieldBlock.php b/core/modules/layout_builder/src/Plugin/Block/ExtraFieldBlock.php index 35c9ee905d893aa586eeddb4eb428f4654374e9a..cf6596077f1dbed71929aa8a40f4c42f7b0df8c4 100644 --- a/core/modules/layout_builder/src/Plugin/Block/ExtraFieldBlock.php +++ b/core/modules/layout_builder/src/Plugin/Block/ExtraFieldBlock.php @@ -2,6 +2,7 @@ namespace Drupal\layout_builder\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Entity\EntityFieldManagerInterface; @@ -11,6 +12,7 @@ use Drupal\Core\Render\Element; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\layout_builder\Plugin\Derivative\ExtraFieldBlockDeriver; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -23,14 +25,13 @@ * This block plugin handles all other field entities not provided by * hook_entity_extra_field_info(). * - * @Block( - * id = "extra_field_block", - * deriver = "\Drupal\layout_builder\Plugin\Derivative\ExtraFieldBlockDeriver", - * ) - * * @internal * Plugin classes are internal. */ +#[Block( + id: "extra_field_block", + deriver: ExtraFieldBlockDeriver::class +)] class ExtraFieldBlock extends BlockBase implements ContextAwarePluginInterface, ContainerFactoryPluginInterface { /** diff --git a/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php index 4318ebee2492a5af941668be66191d2f363286bc..578f9348eb72c8d4e2b1f5b954f75e8e552732a3 100644 --- a/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php +++ b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php @@ -5,6 +5,7 @@ use Drupal\Component\Plugin\Factory\DefaultFactory; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Entity\EntityDisplayBase; @@ -21,6 +22,7 @@ use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\field\FieldLabelOptionsTrait; @@ -28,14 +30,13 @@ /** * Provides a block that renders a field from an entity. * - * @Block( - * id = "field_block", - * deriver = "\Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver", - * ) - * * @internal * Plugin classes are internal. */ +#[Block( + id: "field_block", + deriver: FieldBlockDeriver::class +)] class FieldBlock extends BlockBase implements ContextAwarePluginInterface, ContainerFactoryPluginInterface { use FieldLabelOptionsTrait; diff --git a/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/src/Plugin/Block/FieldBlock.php b/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/src/Plugin/Block/FieldBlock.php index 07a00f5412b70fa258605683591f5130cc5a879f..bb7ac692ee7e1f62dadc7dffb6ddd2fc68900150 100644 --- a/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/src/Plugin/Block/FieldBlock.php +++ b/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/src/Plugin/Block/FieldBlock.php @@ -2,7 +2,9 @@ namespace Drupal\layout_builder_fieldblock_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\layout_builder\Plugin\Block\FieldBlock as LayoutBuilderFieldBlock; +use Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver; /** * Provides test field block to test with Block UI. @@ -14,14 +16,13 @@ * testing, this plugin uses the same deriver but each derivative will have a * different provider. * - * @Block( - * id = "field_block_test", - * deriver = "\Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver", - * ) - * * @see \Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest * @see layout_builder_plugin_filter_block__block_ui_alter() */ +#[Block( + id: "field_block_test", + deriver: FieldBlockDeriver::class +)] class FieldBlock extends LayoutBuilderFieldBlock { } diff --git a/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestFormApiFormBlock.php b/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestFormApiFormBlock.php index 791ccbba9b710203c9888ca780c819244d1394f6..970b24f4896c3edf7d1e7be67e4e0cf24055949c 100644 --- a/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestFormApiFormBlock.php +++ b/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestFormApiFormBlock.php @@ -2,22 +2,23 @@ namespace Drupal\layout_builder_form_block_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a block containing a Form API form for use in Layout Builder tests. - * - * @Block( - * id = "layout_builder_form_block_test_form_api_form_block", - * admin_label = @Translation("Layout Builder form block test form api form block"), - * category = @Translation("Layout Builder form block test") - * ) */ +#[Block( + id: "layout_builder_form_block_test_form_api_form_block", + admin_label: new TranslatableMarkup("Layout Builder form block test form api form block"), + category: new TranslatableMarkup("Layout Builder form block test") +)] class TestFormApiFormBlock extends BlockBase implements ContainerFactoryPluginInterface, FormInterface { /** diff --git a/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestInlineTemplateFormBlock.php b/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestInlineTemplateFormBlock.php index c93f55d3cd4f022634dd0d2b7867eb4900e742a9..4d5582c088cae9c5701e51acd9561458861bf7cc 100644 --- a/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestInlineTemplateFormBlock.php +++ b/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestInlineTemplateFormBlock.php @@ -2,19 +2,20 @@ namespace Drupal\layout_builder_form_block_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a block containing inline template with <form> tag. * * For use in Layout Builder tests. - * - * @Block( - * id = "layout_builder_form_block_test_inline_template_form_block", - * admin_label = @Translation("Layout Builder form block test inline template form block"), - * category = @Translation("Layout Builder form block test") - * ) */ +#[Block( + id: "layout_builder_form_block_test_inline_template_form_block", + admin_label: new TranslatableMarkup("Layout Builder form block test inline template form block"), + category: new TranslatableMarkup("Layout Builder form block test") +)] class TestInlineTemplateFormBlock extends BlockBase { /** diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/IHaveRuntimeContexts.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/IHaveRuntimeContexts.php index e3832a60f77cc095963be31cf1938d0093f2cf1f..d24b8dd5c628d59021a6cac96e8b8f538565bb7c 100644 --- a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/IHaveRuntimeContexts.php +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/IHaveRuntimeContexts.php @@ -2,20 +2,22 @@ namespace Drupal\layout_builder_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Defines a class for a context-aware block. - * - * @Block( - * id = "i_have_runtime_contexts", - * admin_label = "Can I have runtime contexts", - * category = "Test", - * context_definitions = { - * "runtime_contexts" = @ContextDefinition("string", label = "Do you have runtime contexts") - * } - * ) */ +#[Block( + id: "i_have_runtime_contexts", + admin_label: new TranslatableMarkup("Can I have runtime contexts"), + category: new TranslatableMarkup("Test"), + context_definitions: [ + 'runtime_contexts' => new ContextDefinition('string', 'Do you have runtime contexts'), + ] +)] class IHaveRuntimeContexts extends BlockBase { /** diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAjaxBlock.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAjaxBlock.php index 1f1eeb0d7fc366261f8f94250a4dca6f02d849a3..71ac45c6afb5416a243a6762937b877046faf833 100644 --- a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAjaxBlock.php +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAjaxBlock.php @@ -2,18 +2,19 @@ namespace Drupal\layout_builder_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a 'TestAjax' block. - * - * @Block( - * id = "layout_builder_test_testajax", - * admin_label = @Translation("TestAjax"), - * category = @Translation("Test") - * ) */ +#[Block( + id: "layout_builder_test_testajax", + admin_label: new TranslatableMarkup("TestAjax"), + category: new TranslatableMarkup("Test") +)] class TestAjaxBlock extends BlockBase { /** diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAttributesBlock.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAttributesBlock.php index 877fc3bfe7fa51a421893a05bc37a5246dad02ce..57b5cbf55982f055a2a04c160ee96ed05219a293 100644 --- a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAttributesBlock.php +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAttributesBlock.php @@ -2,18 +2,19 @@ namespace Drupal\layout_builder_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a 'TestAttributes' block. - * - * @Block( - * id = "layout_builder_test_test_attributes", - * admin_label = @Translation("Test Attributes"), - * category = @Translation("Test") - * ) */ +#[Block( + id: "layout_builder_test_test_attributes", + admin_label: new TranslatableMarkup("Test Attributes"), + category: new TranslatableMarkup("Test") +)] class TestAttributesBlock extends BlockBase { /** diff --git a/core/modules/node/src/Plugin/Action/AssignOwnerNode.php b/core/modules/node/src/Plugin/Action/AssignOwnerNode.php index 287dcb01898f54ccb4d6c8b12569456c5a0a188c..e764f91a478a6e1dd7c2a8eec9b1bf4cf6b5c051 100644 --- a/core/modules/node/src/Plugin/Action/AssignOwnerNode.php +++ b/core/modules/node/src/Plugin/Action/AssignOwnerNode.php @@ -3,22 +3,23 @@ namespace Drupal\node\Plugin\Action; use Drupal\Core\Action\ConfigurableActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Database\Connection; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\user\Entity\User; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Assigns ownership of a node to a user. - * - * @Action( - * id = "node_assign_owner_action", - * label = @Translation("Change the author of content"), - * type = "node" - * ) */ +#[Action( + id: 'node_assign_owner_action', + label: new TranslatableMarkup('Change the author of content'), + type: 'node' +)] class AssignOwnerNode extends ConfigurableActionBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/node/src/Plugin/Action/DemoteNode.php b/core/modules/node/src/Plugin/Action/DemoteNode.php index efacdb420e950afe099f4f6a2fb20fddb7f0d569..d3de76dfcce80e05e1b11d2896780c9fb4939506 100644 --- a/core/modules/node/src/Plugin/Action/DemoteNode.php +++ b/core/modules/node/src/Plugin/Action/DemoteNode.php @@ -2,18 +2,19 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Field\FieldUpdateActionBase; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\node\NodeInterface; /** * Demotes a node. - * - * @Action( - * id = "node_unpromote_action", - * label = @Translation("Demote selected content from front page"), - * type = "node" - * ) */ +#[Action( + id: 'node_unpromote_action', + label: new TranslatableMarkup('Demote selected content from front page'), + type: 'node' +)] class DemoteNode extends FieldUpdateActionBase { /** diff --git a/core/modules/node/src/Plugin/Action/PromoteNode.php b/core/modules/node/src/Plugin/Action/PromoteNode.php index 1d0e61695db77bbe9ef14132eae0b891e8c577ca..5a539d7bbe2c3c03ec3168c544ab0305e80d58bd 100644 --- a/core/modules/node/src/Plugin/Action/PromoteNode.php +++ b/core/modules/node/src/Plugin/Action/PromoteNode.php @@ -2,18 +2,19 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Field\FieldUpdateActionBase; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\node\NodeInterface; /** * Promotes a node. - * - * @Action( - * id = "node_promote_action", - * label = @Translation("Promote selected content to front page"), - * type = "node" - * ) */ +#[Action( + id: 'node_promote_action', + label: new TranslatableMarkup('Promote selected content to front page'), + type: 'node' +)] class PromoteNode extends FieldUpdateActionBase { /** diff --git a/core/modules/node/src/Plugin/Action/StickyNode.php b/core/modules/node/src/Plugin/Action/StickyNode.php index 679c3c019bb8492412c26c2a4b937bacc4441986..67090c7895f0d57812750ec7b428fc8e5648f0ad 100644 --- a/core/modules/node/src/Plugin/Action/StickyNode.php +++ b/core/modules/node/src/Plugin/Action/StickyNode.php @@ -2,18 +2,19 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Field\FieldUpdateActionBase; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\node\NodeInterface; /** * Makes a node sticky. - * - * @Action( - * id = "node_make_sticky_action", - * label = @Translation("Make selected content sticky"), - * type = "node" - * ) */ +#[Action( + id: 'node_make_sticky_action', + label: new TranslatableMarkup('Make selected content sticky'), + type: 'node' +)] class StickyNode extends FieldUpdateActionBase { /** diff --git a/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php b/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php index dcca814629b7a8aea181447d47a15b557e7f7218..ffe0e5e255b49a3592ce473731310ad71515de92 100644 --- a/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php +++ b/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php @@ -4,18 +4,19 @@ use Drupal\Component\Utility\Tags; use Drupal\Core\Action\ConfigurableActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Unpublishes a node containing certain keywords. - * - * @Action( - * id = "node_unpublish_by_keyword_action", - * label = @Translation("Unpublish content containing keyword(s)"), - * type = "node" - * ) */ +#[Action( + id: 'node_unpublish_by_keyword_action', + label: new TranslatableMarkup('Unpublish content containing keyword(s)'), + type: 'node' +)] class UnpublishByKeywordNode extends ConfigurableActionBase { /** diff --git a/core/modules/node/src/Plugin/Action/UnstickyNode.php b/core/modules/node/src/Plugin/Action/UnstickyNode.php index c074fe9e5a3df8e59abc67d232da3073d3b39e35..0a85dfc772011dee8a9d90df94c0f3686d70f802 100644 --- a/core/modules/node/src/Plugin/Action/UnstickyNode.php +++ b/core/modules/node/src/Plugin/Action/UnstickyNode.php @@ -2,18 +2,19 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Field\FieldUpdateActionBase; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\node\NodeInterface; /** * Makes a node not sticky. - * - * @Action( - * id = "node_make_unsticky_action", - * label = @Translation("Make selected content not sticky"), - * type = "node" - * ) */ +#[Action( + id: 'node_make_unsticky_action', + label: new TranslatableMarkup('Make selected content not sticky'), + type: 'node' +)] class UnstickyNode extends FieldUpdateActionBase { /** diff --git a/core/modules/node/src/Plugin/Block/SyndicateBlock.php b/core/modules/node/src/Plugin/Block/SyndicateBlock.php index 90af1d54b46787b1049df9496c376607b73bca18..8dea77dc11b6b403d4803b33647a7f4f85a04295 100644 --- a/core/modules/node/src/Plugin/Block/SyndicateBlock.php +++ b/core/modules/node/src/Plugin/Block/SyndicateBlock.php @@ -3,8 +3,10 @@ namespace Drupal\node\Plugin\Block; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Session\AccountInterface; @@ -12,13 +14,12 @@ /** * Provides a 'Syndicate' block that links to the site's RSS feed. - * - * @Block( - * id = "node_syndicate_block", - * admin_label = @Translation("Syndicate"), - * category = @Translation("System") - * ) */ +#[Block( + id: "node_syndicate_block", + admin_label: new TranslatableMarkup("Syndicate"), + category: new TranslatableMarkup("System") +)] class SyndicateBlock extends BlockBase implements ContainerFactoryPluginInterface { diff --git a/core/modules/node/tests/modules/node_block_test/src/Plugin/Block/NodeContextTestBlock.php b/core/modules/node/tests/modules/node_block_test/src/Plugin/Block/NodeContextTestBlock.php index 01d157d9e90b944739cf44f1bd1b7ea74759ff3e..1c303f8d0ab82f2e6ef1ee9e133d68db2a74ddd3 100644 --- a/core/modules/node/tests/modules/node_block_test/src/Plugin/Block/NodeContextTestBlock.php +++ b/core/modules/node/tests/modules/node_block_test/src/Plugin/Block/NodeContextTestBlock.php @@ -2,19 +2,21 @@ namespace Drupal\node_block_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\Plugin\Context\EntityContextDefinition; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a 'Node Context Test' block. - * - * @Block( - * id = "node_block_test_context", - * label = @Translation("Node Context Test"), - * context_definitions = { - * "node" = @ContextDefinition("entity:node", label = @Translation("Node")) - * } - * ) */ +#[Block( + id: "node_block_test_context", + admin_label: new TranslatableMarkup("Node Context Test"), + context_definitions: [ + 'node' => new EntityContextDefinition('entity:node', new TranslatableMarkup("Node")), + ] +)] class NodeContextTestBlock extends BlockBase { /** diff --git a/core/modules/search/src/Plugin/Block/SearchBlock.php b/core/modules/search/src/Plugin/Block/SearchBlock.php index d1a1f249fd22f7d9333f4bf41ba8c8aef980fa48..5afe788baaa410f6f035b797154aec4c85af9755 100644 --- a/core/modules/search/src/Plugin/Block/SearchBlock.php +++ b/core/modules/search/src/Plugin/Block/SearchBlock.php @@ -3,24 +3,25 @@ namespace Drupal\search\Plugin\Block; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\search\Form\SearchBlockForm; use Drupal\search\SearchPageRepositoryInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a 'Search form' block. - * - * @Block( - * id = "search_form_block", - * admin_label = @Translation("Search form"), - * category = @Translation("Forms") - * ) */ +#[Block( + id: "search_form_block", + admin_label: new TranslatableMarkup("Search form"), + category: new TranslatableMarkup("Forms"), +)] class SearchBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsClassBlock.php b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsClassBlock.php index 699a09a501b04415823966882f2c4b0a5bb01e4f..78ab6957a028ed5c7f213b51e8626babd39449f8 100644 --- a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsClassBlock.php +++ b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsClassBlock.php @@ -2,19 +2,21 @@ namespace Drupal\settings_tray_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\settings_tray_test\Form\SettingsTrayFormAnnotationIsClassBlockForm; /** * Block that explicitly provides a "settings_tray" form class. - * - * @Block( - * id = "settings_tray_test_class", - * admin_label = "Settings Tray test block: forms[settings_tray]=class", - * forms = { - * "settings_tray" = "\Drupal\settings_tray_test\Form\SettingsTrayFormAnnotationIsClassBlockForm", - * }, - * ) */ +#[Block( + id: "settings_tray_test_class", + admin_label: new TranslatableMarkup("Settings Tray test block: forms[settings_tray]=class"), + forms: [ + 'settings_tray' => SettingsTrayFormAnnotationIsClassBlockForm::class, + ] +)] class SettingsTrayFormAnnotationIsClassBlock extends BlockBase { /** diff --git a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsFalseBlock.php b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsFalseBlock.php index 47bb6113bd369a0f0e4447045f482bd8dbf4e3b2..65661416c327f5a75dd090abf7279e25bebcdd36 100644 --- a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsFalseBlock.php +++ b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsFalseBlock.php @@ -2,19 +2,20 @@ namespace Drupal\settings_tray_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Block that explicitly provides no "settings_tray" form, thus opting out. - * - * @Block( - * id = "settings_tray_test_false", - * admin_label = "Settings Tray test block: forms[settings_tray]=FALSE", - * forms = { - * "settings_tray" = FALSE, - * }, - * ) */ +#[Block( + id: "settings_tray_test_false", + admin_label: new TranslatableMarkup("Settings Tray test block: forms[settings_tray]=FALSE"), + forms: [ + 'settings_tray' => FALSE, + ] +)] class SettingsTrayFormAnnotationIsFalseBlock extends BlockBase { /** diff --git a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationNoneBlock.php b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationNoneBlock.php index 5b6eb923683065d68fd7712028afe0ba71115d17..1302646bf0bb6521b2ed14f9e41e864cf5600639 100644 --- a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationNoneBlock.php +++ b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationNoneBlock.php @@ -2,16 +2,17 @@ namespace Drupal\settings_tray_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Block that does nothing explicit for Settings Tray. - * - * @Block( - * id = "settings_tray_test_none", - * admin_label = "Settings Tray test block: forms[settings_tray] is not specified", - * ) */ +#[Block( + id: "settings_tray_test_none", + admin_label: new TranslatableMarkup("Settings Tray test block: forms[settings_tray] is not specified") +)] class SettingsTrayFormAnnotationNoneBlock extends BlockBase { /** diff --git a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/ValidationErrorBlock.php b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/ValidationErrorBlock.php index 280e8c7f64f9eac6dfb367ab6f27aad7d90a132c..fd5f1d1533b2f88b03e34f5713f954c35c64b299 100644 --- a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/ValidationErrorBlock.php +++ b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/ValidationErrorBlock.php @@ -2,17 +2,18 @@ namespace Drupal\settings_tray_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a 'Block with validation error' test block. - * - * @Block( - * id = "settings_tray_test_validation", - * admin_label = @Translation("Block with validation error") - * ) */ +#[Block( + id: "settings_tray_test_validation", + admin_label: new TranslatableMarkup("Block with validation error"), +)] class ValidationErrorBlock extends BlockBase { /** diff --git a/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php b/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php index ab2d57e414db0f343298bc94d5b90c2515f3259b..f11f836b43694124e13104682d8a6c2ae4595eae 100644 --- a/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php +++ b/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php @@ -3,18 +3,19 @@ namespace Drupal\shortcut\Plugin\Block; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a 'Shortcut' block. - * - * @Block( - * id = "shortcuts", - * admin_label = @Translation("Shortcuts"), - * category = @Translation("Menus") - * ) */ +#[Block( + id: "shortcuts", + admin_label: new TranslatableMarkup("Shortcuts"), + category: new TranslatableMarkup("Menus") +)] class ShortcutsBlock extends BlockBase { /** diff --git a/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php index 072c72736f404476d794b7f1c055a90fa5f6df5a..82eeb68552e2fcfdec998dabf8e6163fe9ac60be 100644 --- a/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php @@ -3,24 +3,25 @@ namespace Drupal\statistics\Plugin\Block; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\statistics\StatisticsStorageInterface; /** * Provides a 'Popular content' block. - * - * @Block( - * id = "statistics_popular_block", - * admin_label = @Translation("Popular content") - * ) */ +#[Block( + id: "statistics_popular_block", + admin_label: new TranslatableMarkup("Popular content"), +)] class StatisticsPopularBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php index 91eaf97dc28bd69afa58dc4d4132cd7a5f07a058..560afe5f57d4193ebf82ffe73b12ed2b5e17f618 100644 --- a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php @@ -2,25 +2,25 @@ 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 = @Translation("Site branding"), - * forms = { - * "settings_tray" = "Drupal\system\Form\SystemBrandingOffCanvasForm", - * }, - * ) */ +#[Block( + id: "system_branding_block", + admin_label: new TranslatableMarkup("Site branding"), + forms: ['settings_tray' => SystemBrandingOffCanvasForm::class] +)] class SystemBrandingBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php b/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php index a08738636c051f70869685b679e41526401746ee..f57d4eb643e8b2d2631a4f6f643addc53e1f9569 100644 --- a/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php @@ -2,20 +2,21 @@ namespace Drupal\system\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a block to display the breadcrumbs. - * - * @Block( - * id = "system_breadcrumb_block", - * admin_label = @Translation("Breadcrumbs") - * ) */ +#[Block( + id: "system_breadcrumb_block", + admin_label: new TranslatableMarkup("Breadcrumbs") +)] class SystemBreadcrumbBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/system/src/Plugin/Block/SystemMainBlock.php b/core/modules/system/src/Plugin/Block/SystemMainBlock.php index 92f443081284016ae51797aadbbe9d76bafc3e12..beb6b5982353d715c605ee3a31b51fc2e269d727 100644 --- a/core/modules/system/src/Plugin/Block/SystemMainBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemMainBlock.php @@ -2,20 +2,21 @@ namespace Drupal\system\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Block\MainContentBlockPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a 'Main page content' block. - * - * @Block( - * id = "system_main_block", - * admin_label = @Translation("Main page content"), - * forms = { - * "settings_tray" = FALSE, - * }, - * ) */ +#[Block( + id: "system_main_block", + admin_label: new TranslatableMarkup("Main page content"), + forms: [ + 'settings_tray' => FALSE, + ] +)] class SystemMainBlock extends BlockBase implements MainContentBlockPluginInterface { /** diff --git a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php index 463b6800b8ccf261137949e9968840df87e313d3..8918d4b2afaee87f0dabe737cd3ab1cf0973a795 100644 --- a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php @@ -2,6 +2,7 @@ namespace Drupal\system\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\Cache; use Drupal\Core\Form\FormStateInterface; @@ -9,21 +10,23 @@ use Drupal\Core\Menu\MenuLinkTreeInterface; use Drupal\Core\Menu\MenuTreeParameters; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\system\Form\SystemMenuOffCanvasForm; +use Drupal\system\Plugin\Derivative\SystemMenuBlock as SystemMenuBlockDeriver; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a generic Menu block. - * - * @Block( - * id = "system_menu_block", - * admin_label = @Translation("Menu"), - * category = @Translation("Menus"), - * deriver = "Drupal\system\Plugin\Derivative\SystemMenuBlock", - * forms = { - * "settings_tray" = "\Drupal\system\Form\SystemMenuOffCanvasForm", - * }, - * ) */ +#[Block( + id: "system_menu_block", + admin_label: new TranslatableMarkup("Menu"), + category: new TranslatableMarkup("Menus"), + deriver: SystemMenuBlockDeriver::class, + forms: [ + 'settings_tray' => SystemMenuOffCanvasForm::class, + ] +)] class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/system/src/Plugin/Block/SystemMessagesBlock.php b/core/modules/system/src/Plugin/Block/SystemMessagesBlock.php index f737b8fd5db6ce603afe0aae3862b7e51094b35c..d6f1a9f22758baaf01177e671cd1788d6ea72810 100644 --- a/core/modules/system/src/Plugin/Block/SystemMessagesBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemMessagesBlock.php @@ -2,20 +2,21 @@ namespace Drupal\system\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Block\MessagesBlockPluginInterface; use Drupal\Core\Cache\Cache; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a block to display the messages. * * @see @see \Drupal\Core\Messenger\MessengerInterface - * - * @Block( - * id = "system_messages_block", - * admin_label = @Translation("Messages") - * ) */ +#[Block( + id: "system_messages_block", + admin_label: new TranslatableMarkup("Messages") +)] class SystemMessagesBlock extends BlockBase implements MessagesBlockPluginInterface { /** diff --git a/core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php b/core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php index c1e0ba8b8c172e35346d8554389ccff9f62963af..513bb11f7f9a870400f9560f7e8f669c0590b34c 100644 --- a/core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php @@ -3,15 +3,16 @@ namespace Drupal\system\Plugin\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\Block\Attribute\Block; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a 'Powered by Drupal' block. - * - * @Block( - * id = "system_powered_by_block", - * admin_label = @Translation("Powered by Drupal") - * ) */ +#[Block( + id: "system_powered_by_block", + admin_label: new TranslatableMarkup("Powered by Drupal") +)] class SystemPoweredByBlock extends BlockBase { /** diff --git a/core/modules/system/tests/modules/action_test/src/Plugin/Action/NoType.php b/core/modules/system/tests/modules/action_test/src/Plugin/Action/NoType.php index 1fc39932bcb00d96ce3d7aee01f2103e8413b4e6..57bcec1ef3be55a23ce731f22b3a9dd544d8452f 100644 --- a/core/modules/system/tests/modules/action_test/src/Plugin/Action/NoType.php +++ b/core/modules/system/tests/modules/action_test/src/Plugin/Action/NoType.php @@ -4,16 +4,17 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides an operation with no type specified. - * - * @Action( - * id = "action_test_no_type", - * label = @Translation("An operation with no type specified") - * ) */ +#[Action( + id: 'action_test_no_type', + label: new TranslatableMarkup('An operation with no type specified'), +)] class NoType extends ActionBase { /** diff --git a/core/modules/system/tests/modules/action_test/src/Plugin/Action/SaveEntity.php b/core/modules/system/tests/modules/action_test/src/Plugin/Action/SaveEntity.php index b20f424ce9bca3646d91423c7bdeaae56c0433e5..2bc7e83c4078402770bd976a7e44f1ff75931c62 100644 --- a/core/modules/system/tests/modules/action_test/src/Plugin/Action/SaveEntity.php +++ b/core/modules/system/tests/modules/action_test/src/Plugin/Action/SaveEntity.php @@ -3,17 +3,18 @@ namespace Drupal\action_test\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides an operation to save user entities. - * - * @Action( - * id = "action_test_save_entity", - * label = @Translation("Saves entities"), - * type = "user" - * ) */ +#[Action( + id: 'action_test_save_entity', + label: new TranslatableMarkup('Saves entities'), + type: 'user' +)] class SaveEntity extends ActionBase { /** diff --git a/core/modules/system/tests/modules/ajax_forms_test/src/Plugin/Block/AjaxFormBlock.php b/core/modules/system/tests/modules/ajax_forms_test/src/Plugin/Block/AjaxFormBlock.php index 65e1372d999f0fc9d47131a1841a6519cd81fdab..8e0d1c112b3958715fd9b01fcf78a1dd2950bd5b 100644 --- a/core/modules/system/tests/modules/ajax_forms_test/src/Plugin/Block/AjaxFormBlock.php +++ b/core/modules/system/tests/modules/ajax_forms_test/src/Plugin/Block/AjaxFormBlock.php @@ -2,23 +2,24 @@ namespace Drupal\ajax_forms_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides an AJAX form block. - * - * @Block( - * id = "ajax_forms_test_block", - * admin_label = @Translation("AJAX test form"), - * category = @Translation("Forms") - * ) */ +#[Block( + id: "ajax_forms_test_block", + admin_label: new TranslatableMarkup("AJAX test form"), + category: new TranslatableMarkup("Forms") +)] class AjaxFormBlock extends BlockBase implements FormInterface, ContainerFactoryPluginInterface { /** diff --git a/core/modules/system/tests/modules/form_test/src/Plugin/Block/RedirectFormBlock.php b/core/modules/system/tests/modules/form_test/src/Plugin/Block/RedirectFormBlock.php index 027be19fe3a0e596c1c1f30efabf8a791903de24..e8ee69fe5419ad13d3bccff674718ee3a7b0c93b 100644 --- a/core/modules/system/tests/modules/form_test/src/Plugin/Block/RedirectFormBlock.php +++ b/core/modules/system/tests/modules/form_test/src/Plugin/Block/RedirectFormBlock.php @@ -2,22 +2,23 @@ namespace Drupal\form_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a block containing a simple redirect form. * * @see \Drupal\form_test\Form\RedirectBlockForm - * - * @Block( - * id = "redirect_form_block", - * admin_label = @Translation("Redirecting form"), - * category = @Translation("Forms") - * ) */ +#[Block( + id: "redirect_form_block", + admin_label: new TranslatableMarkup("Redirecting form"), + category: new TranslatableMarkup("Forms"), +)] class RedirectFormBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/system/tests/modules/plugin_test/src/Plugin/Attribute/PluginExample.php b/core/modules/system/tests/modules/plugin_test/src/Plugin/Attribute/PluginExample.php new file mode 100644 index 0000000000000000000000000000000000000000..af916bb90dba9cd301ecc333351a4652c9f47134 --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/src/Plugin/Attribute/PluginExample.php @@ -0,0 +1,26 @@ +<?php + +namespace Drupal\plugin_test\Plugin\Attribute; + +use Drupal\Component\Plugin\Attribute\Plugin; + +/** + * Defines a custom PluginExample attribute. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class PluginExample extends Plugin { + + /** + * Constructs a PluginExample attribute. + * + * @param string $id + * The plugin ID. + * @param string $custom + * Some other sample plugin metadata. + */ + public function __construct( + public readonly string $id, + public readonly ?string $custom = NULL + ) {} + +} diff --git a/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example3.php b/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example3.php new file mode 100644 index 0000000000000000000000000000000000000000..2394b85c2de76ae00b44ce21bea8e194859cefd8 --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example3.php @@ -0,0 +1,14 @@ +<?php + +namespace Drupal\plugin_test\Plugin\plugin_test\custom_annotation; + +use Drupal\plugin_test\Plugin\Attribute\PluginExample; + +/** + * Provides a test plugin with a custom attribute. + */ +#[PluginExample( + id: "example_3", + custom: "George" +)] +class Example3 {} diff --git a/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/AttachedRenderingBlock.php b/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/AttachedRenderingBlock.php index 721d10cafe6a4345f0f1eef2bd18ec729da89af4..c660a4ff7783d3b2447514b4255f980fba484078 100644 --- a/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/AttachedRenderingBlock.php +++ b/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/AttachedRenderingBlock.php @@ -2,6 +2,8 @@ namespace Drupal\render_attached_test\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\render_attached_test\Controller\RenderAttachedTestController; use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\Cache; @@ -10,13 +12,12 @@ /** * A block we can use to test caching of #attached headers. * - * @Block( - * id = "attached_rendering_block", - * admin_label = @Translation("AttachedRenderingBlock") - * ) - * * @see \Drupal\system\Tests\Render\HtmlResponseAttachmentsTest */ +#[Block( + id: "attached_rendering_block", + admin_label: new TranslatableMarkup("AttachedRenderingBlock") +)] class AttachedRenderingBlock extends BlockBase { /** diff --git a/core/modules/user/src/Plugin/Action/AddRoleUser.php b/core/modules/user/src/Plugin/Action/AddRoleUser.php index 6980ff9b44ce69805b90449e79f3ff237f25475c..f8be14d9225836fd95d44d01daf51ec37e528918 100644 --- a/core/modules/user/src/Plugin/Action/AddRoleUser.php +++ b/core/modules/user/src/Plugin/Action/AddRoleUser.php @@ -2,15 +2,17 @@ namespace Drupal\user\Plugin\Action; +use Drupal\Core\Action\Attribute\Action; +use Drupal\Core\StringTranslation\TranslatableMarkup; + /** * Adds a role to a user. - * - * @Action( - * id = "user_add_role_action", - * label = @Translation("Add a role to the selected users"), - * type = "user" - * ) */ +#[Action( + id: 'user_add_role_action', + label: new TranslatableMarkup('Add a role to the selected users'), + type: 'user' +)] class AddRoleUser extends ChangeUserRoleBase { /** diff --git a/core/modules/user/src/Plugin/Action/BlockUser.php b/core/modules/user/src/Plugin/Action/BlockUser.php index 6a875f78d95a79b0214a8a220e229c6ee04f10c7..560c97e33bd91e1d8fb178861ac0a10e1fd6d4ec 100644 --- a/core/modules/user/src/Plugin/Action/BlockUser.php +++ b/core/modules/user/src/Plugin/Action/BlockUser.php @@ -3,17 +3,18 @@ namespace Drupal\user\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Blocks a user. - * - * @Action( - * id = "user_block_user_action", - * label = @Translation("Block the selected users"), - * type = "user" - * ) */ +#[Action( + id: 'user_block_user_action', + label: new TranslatableMarkup('Block the selected users'), + type: 'user' +)] class BlockUser extends ActionBase { /** diff --git a/core/modules/user/src/Plugin/Action/CancelUser.php b/core/modules/user/src/Plugin/Action/CancelUser.php index 0fa74c021bcf1e3a70b5cc0828b914336f3a09ce..68ad56ad9c6fcc7794716e9e505df7d3a2ab7f09 100644 --- a/core/modules/user/src/Plugin/Action/CancelUser.php +++ b/core/modules/user/src/Plugin/Action/CancelUser.php @@ -3,21 +3,22 @@ namespace Drupal\user\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\TempStore\PrivateTempStoreFactory; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Cancels a user account. - * - * @Action( - * id = "user_cancel_user_action", - * label = @Translation("Cancel the selected user accounts"), - * type = "user", - * confirm_form_route_name = "user.multiple_cancel_confirm" - * ) */ +#[Action( + id: 'user_cancel_user_action', + label: new TranslatableMarkup('Cancel the selected user accounts'), + type: 'user', + confirm_form_route_name: 'user.multiple_cancel_confirm' +)] class CancelUser extends ActionBase implements ContainerFactoryPluginInterface { /** diff --git a/core/modules/user/src/Plugin/Action/RemoveRoleUser.php b/core/modules/user/src/Plugin/Action/RemoveRoleUser.php index 66cf6d0f3637b4db2a3b291a510a65224f2c8028..1c6e1186702c6b086c9e8707acb27d0b0270caef 100644 --- a/core/modules/user/src/Plugin/Action/RemoveRoleUser.php +++ b/core/modules/user/src/Plugin/Action/RemoveRoleUser.php @@ -2,15 +2,17 @@ namespace Drupal\user\Plugin\Action; +use Drupal\Core\Action\Attribute\Action; +use Drupal\Core\StringTranslation\TranslatableMarkup; + /** * Removes a role from a user. - * - * @Action( - * id = "user_remove_role_action", - * label = @Translation("Remove a role from the selected users"), - * type = "user" - * ) */ +#[Action( + id: 'user_remove_role_action', + label: new TranslatableMarkup('Remove a role from the selected users'), + type: 'user' +)] class RemoveRoleUser extends ChangeUserRoleBase { /** diff --git a/core/modules/user/src/Plugin/Action/UnblockUser.php b/core/modules/user/src/Plugin/Action/UnblockUser.php index 68c59b40e161084254e89f769a3ed6641bfe4f6e..6df7686731bc5c12b6848ad0e502aab6cb887091 100644 --- a/core/modules/user/src/Plugin/Action/UnblockUser.php +++ b/core/modules/user/src/Plugin/Action/UnblockUser.php @@ -3,17 +3,18 @@ namespace Drupal\user\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Unblocks a user. - * - * @Action( - * id = "user_unblock_user_action", - * label = @Translation("Unblock the selected users"), - * type = "user" - * ) */ +#[Action( + id: 'user_unblock_user_action', + label: new TranslatableMarkup('Unblock the selected users'), + type: 'user' +)] class UnblockUser extends ActionBase { /** diff --git a/core/modules/user/src/Plugin/Block/UserLoginBlock.php b/core/modules/user/src/Plugin/Block/UserLoginBlock.php index e35295a59069268248f7917e8aa9b343121cb56b..431d9edd4aab36aee14d9bba7d81eced2728051e 100644 --- a/core/modules/user/src/Plugin/Block/UserLoginBlock.php +++ b/core/modules/user/src/Plugin/Block/UserLoginBlock.php @@ -4,10 +4,12 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Routing\RedirectDestinationTrait; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Block\BlockBase; @@ -16,13 +18,12 @@ /** * Provides a 'User login' block. - * - * @Block( - * id = "user_login_block", - * admin_label = @Translation("User login"), - * category = @Translation("Forms") - * ) */ +#[Block( + id: "user_login_block", + admin_label: new TranslatableMarkup("User login"), + category: new TranslatableMarkup("Forms") +)] class UserLoginBlock extends BlockBase implements ContainerFactoryPluginInterface, TrustedCallbackInterface { use RedirectDestinationTrait; diff --git a/core/modules/views/src/Plugin/Block/ViewsBlock.php b/core/modules/views/src/Plugin/Block/ViewsBlock.php index 34738cddd4d45cba0fda6e1bb2d11fceb284869c..8c4bd7542cf9a673d271872978443e9931f9cb5f 100644 --- a/core/modules/views/src/Plugin/Block/ViewsBlock.php +++ b/core/modules/views/src/Plugin/Block/ViewsBlock.php @@ -3,19 +3,21 @@ namespace Drupal\views\Plugin\Block; use Drupal\Component\Utility\Xss; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\views\Element\View; +use Drupal\views\Plugin\Derivative\ViewsBlock as ViewsBlockDeriver; use Drupal\Core\Entity\EntityInterface; /** * Provides a generic Views block. - * - * @Block( - * id = "views_block", - * admin_label = @Translation("Views Block"), - * deriver = "Drupal\views\Plugin\Derivative\ViewsBlock" - * ) */ +#[Block( + id: "views_block", + admin_label: new TranslatableMarkup("Views Block"), + deriver: ViewsBlockDeriver::class +)] class ViewsBlock extends ViewsBlockBase { /** diff --git a/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php b/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php index f43fda6d498ab6eb3b9892fb6d1ce9d9d34ae0a0..135c4313abad1b7b81554cd3b7592e961f7830c8 100644 --- a/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php +++ b/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php @@ -2,18 +2,20 @@ namespace Drupal\views\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Cache\Cache; use Drupal\Component\Utility\Xss; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\views\Plugin\Derivative\ViewsExposedFilterBlock as ViewsExposedFilterBlockDeriver; /** * Provides a 'Views Exposed Filter' block. - * - * @Block( - * id = "views_exposed_filter_block", - * admin_label = @Translation("Views Exposed Filter Block"), - * deriver = "Drupal\views\Plugin\Derivative\ViewsExposedFilterBlock" - * ) */ +#[Block( + id: "views_exposed_filter_block", + admin_label: new TranslatableMarkup("Views Exposed Filter Block"), + deriver: ViewsExposedFilterBlockDeriver::class +)] class ViewsExposedFilterBlock extends ViewsBlockBase { /** diff --git a/core/modules/views/tests/modules/user_batch_action_test/src/Plugin/Action/BatchUserAction.php b/core/modules/views/tests/modules/user_batch_action_test/src/Plugin/Action/BatchUserAction.php index 8c054dead34ef25472d10866722595f144dd298f..9e58ca8abeb501ae5b78b7e0a848dd90b20dd1a8 100644 --- a/core/modules/views/tests/modules/user_batch_action_test/src/Plugin/Action/BatchUserAction.php +++ b/core/modules/views/tests/modules/user_batch_action_test/src/Plugin/Action/BatchUserAction.php @@ -3,18 +3,19 @@ namespace Drupal\user_batch_action_test\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Action\Attribute\Action; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides action that sets batch precessing. - * - * @Action( - * id = "user_batch_action_test_action", - * label = @Translation("Process user in batch"), - * type = "user", - * ) */ +#[Action( + id: 'user_batch_action_test_action', + label: new TranslatableMarkup('Process user in batch'), + type: 'user' +)] class BatchUserAction extends ActionBase { /** diff --git a/core/modules/workspaces/src/Plugin/Block/WorkspaceSwitcherBlock.php b/core/modules/workspaces/src/Plugin/Block/WorkspaceSwitcherBlock.php index 2f5a2fe743a3957153809df9f9845af75e107ec5..0333524db6aa98c6187e4971a654f7394d1c3900 100644 --- a/core/modules/workspaces/src/Plugin/Block/WorkspaceSwitcherBlock.php +++ b/core/modules/workspaces/src/Plugin/Block/WorkspaceSwitcherBlock.php @@ -2,22 +2,23 @@ namespace Drupal\workspaces\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\workspaces\Form\WorkspaceSwitcherForm; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a 'Workspace switcher' block. - * - * @Block( - * id = "workspace_switcher", - * admin_label = @Translation("Workspace switcher"), - * category = @Translation("Workspace"), - * ) */ +#[Block( + id: "workspace_switcher", + admin_label: new TranslatableMarkup("Workspace switcher"), + category: new TranslatableMarkup("Workspace") +)] class WorkspaceSwitcherBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/core/tests/Drupal/KernelTests/Core/Plugin/DefaultPluginManagerTest.php b/core/tests/Drupal/KernelTests/Core/Plugin/DefaultPluginManagerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a4566a1a3dc8f3e0e098201b9699d84e17877fcd --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Plugin/DefaultPluginManagerTest.php @@ -0,0 +1,53 @@ +<?php + +namespace Drupal\KernelTests\Core\Plugin; + +use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\KernelTests\KernelTestBase; +use Drupal\plugin_test\Plugin\Annotation\PluginExample as AnnotationPluginExample; +use Drupal\plugin_test\Plugin\Attribute\PluginExample as AttributePluginExample; + +/** + * Tests the default plugin manager. + * + * @group Plugin + */ +class DefaultPluginManagerTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['plugin_test']; + + /** + * Tests annotations and attributes on the default plugin manager. + */ + public function testDefaultPluginManager() { + $subdir = 'Plugin/plugin_test/custom_annotation'; + $base_directory = $this->root . '/core/modules/system/tests/modules/plugin_test/src'; + $namespaces = new \ArrayObject(['Drupal\plugin_test' => $base_directory]); + $module_handler = $this->container->get('module_handler'); + + // Annotation only. + $manager = new DefaultPluginManager($subdir, $namespaces, $module_handler, NULL, AnnotationPluginExample::class); + $definitions = $manager->getDefinitions(); + $this->assertArrayHasKey('example_1', $definitions); + $this->assertArrayHasKey('example_2', $definitions); + $this->assertArrayNotHasKey('example_3', $definitions); + + // Annotations and attributes together. + $manager = new DefaultPluginManager($subdir, $namespaces, $module_handler, NULL, AttributePluginExample::class, AnnotationPluginExample::class); + $definitions = $manager->getDefinitions(); + $this->assertArrayHasKey('example_1', $definitions); + $this->assertArrayHasKey('example_2', $definitions); + $this->assertArrayHasKey('example_3', $definitions); + + // Attributes only. + $manager = new DefaultPluginManager($subdir, $namespaces, $module_handler, NULL, AttributePluginExample::class); + $definitions = $manager->getDefinitions(); + $this->assertArrayNotHasKey('example_1', $definitions); + $this->assertArrayNotHasKey('example_2', $definitions); + $this->assertArrayHasKey('example_3', $definitions); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeBaseTest.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeBaseTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1f9a5657a66ae79a4efaad01c059a245cb3aa5a8 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeBaseTest.php @@ -0,0 +1,48 @@ +<?php + +namespace Drupal\Tests\Component\Plugin\Attribute; + +use Drupal\Component\Plugin\Attribute\AttributeBase; +use PHPUnit\Framework\TestCase; + +/** + * @coversDefaultClass \Drupal\Component\Plugin\Attribute\AttributeBase + * @group Attribute + */ +class AttributeBaseTest extends TestCase { + + /** + * @covers ::getProvider + * @covers ::setProvider + */ + public function testSetProvider() { + $plugin = new AttributeBaseStub(id: '1'); + $plugin->setProvider('example'); + $this->assertEquals('example', $plugin->getProvider()); + } + + /** + * @covers ::getId + */ + public function testGetId() { + $plugin = new AttributeBaseStub(id: 'example'); + $this->assertEquals('example', $plugin->getId()); + } + + /** + * @covers ::getClass + * @covers ::setClass + */ + public function testSetClass() { + $plugin = new AttributeBaseStub(id: '1'); + $plugin->setClass('example'); + $this->assertEquals('example', $plugin->getClass()); + } + +} +/** + * {@inheritdoc} + */ +class AttributeBaseStub extends AttributeBase { + +} diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d68a724b566f4ec516678dfc61c13426e8332e39 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php @@ -0,0 +1,71 @@ +<?php + +namespace Drupal\Tests\Component\Plugin\Attribute; + +use Drupal\Component\Plugin\Discovery\AttributeClassDiscovery; +use Drupal\Component\FileCache\FileCacheFactory; +use PHPUnit\Framework\TestCase; + +/** + * @coversDefaultClass \Drupal\Component\Plugin\Discovery\AttributeClassDiscovery + * @group Attribute + * @runTestsInSeparateProcesses + */ +class AttributeClassDiscoveryCachedTest extends TestCase { + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + // Ensure FileCacheFactory::DISABLE_CACHE is *not* set, since we're testing + // integration with the file cache. + FileCacheFactory::setConfiguration([]); + // Ensure that FileCacheFactory has a prefix. + FileCacheFactory::setPrefix('prefix'); + + // Normally the attribute classes would be autoloaded. + include_once __DIR__ . '/Fixtures/CustomPlugin.php'; + include_once __DIR__ . '/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php'; + } + + /** + * Tests that getDefinitions() retrieves the file cache correctly. + * + * @covers ::getDefinitions + */ + public function testGetDefinitions() { + // Path to the classes which we'll discover and parse annotation. + $discovery_path = __DIR__ . '/Fixtures/Plugins'; + // File path that should be discovered within that directory. + $file_path = $discovery_path . '/PluginNamespace/AttributeDiscoveryTest1.php'; + + $discovery = new AttributeClassDiscovery(['com\example' => [$discovery_path]]); + $this->assertEquals([ + 'discovery_test_1' => [ + 'id' => 'discovery_test_1', + 'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1', + ], + ], $discovery->getDefinitions()); + + // Gain access to the file cache so we can change it. + $ref_file_cache = new \ReflectionProperty($discovery, 'fileCache'); + $ref_file_cache->setAccessible(TRUE); + /** @var \Drupal\Component\FileCache\FileCacheInterface $file_cache */ + $file_cache = $ref_file_cache->getValue($discovery); + // The file cache is keyed by the file path, and we'll add some known + // content to test against. + $file_cache->set($file_path, [ + 'id' => 'wrong_id', + 'content' => serialize(['an' => 'array']), + ]); + + // Now perform the same query and check for the cached results. + $this->assertEquals([ + 'wrong_id' => [ + 'an' => 'array', + ], + ], $discovery->getDefinitions()); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryTest.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..965254c719e3410af2651791b581667324e65020 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryTest.php @@ -0,0 +1,74 @@ +<?php + +namespace Drupal\Tests\Component\Plugin\Attribute; + +use Drupal\Component\Plugin\Discovery\AttributeClassDiscovery; +use Drupal\Component\FileCache\FileCacheFactory; +use PHPUnit\Framework\TestCase; +use com\example\PluginNamespace\CustomPlugin; +use com\example\PluginNamespace\CustomPlugin2; + +/** + * @coversDefaultClass \Drupal\Component\Plugin\Discovery\AttributeClassDiscovery + * @group Attribute + * @runTestsInSeparateProcesses + */ +class AttributeClassDiscoveryTest extends TestCase { + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + // Ensure the file cache is disabled. + FileCacheFactory::setConfiguration([FileCacheFactory::DISABLE_CACHE => TRUE]); + // Ensure that FileCacheFactory has a prefix. + FileCacheFactory::setPrefix('prefix'); + + // Normally the attribute classes would be autoloaded. + include_once __DIR__ . '/Fixtures/CustomPlugin.php'; + include_once __DIR__ . '/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php'; + } + + /** + * @covers ::__construct + * @covers ::getPluginNamespaces + */ + public function testGetPluginNamespaces() { + // Path to the classes which we'll discover and parse annotation. + $discovery = new AttributeClassDiscovery(['com/example' => [__DIR__]]); + + $reflection = new \ReflectionMethod($discovery, 'getPluginNamespaces'); + $reflection->setAccessible(TRUE); + + $result = $reflection->invoke($discovery); + $this->assertEquals(['com/example' => [__DIR__]], $result); + } + + /** + * @covers ::getDefinitions + * @covers ::prepareAttributeDefinition + */ + public function testGetDefinitions() { + $discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . '/Fixtures/Plugins']]); + $this->assertEquals([ + 'discovery_test_1' => [ + 'id' => 'discovery_test_1', + 'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1', + ], + ], $discovery->getDefinitions()); + + $custom_annotation_discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . '/Fixtures/Plugins']], CustomPlugin::class); + $this->assertEquals([ + 'discovery_test_1' => [ + 'id' => 'discovery_test_1', + 'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1', + 'title' => 'Discovery test plugin', + ], + ], $custom_annotation_discovery->getDefinitions()); + + $empty_discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . '/Fixtures/Plugins']], CustomPlugin2::class); + $this->assertEquals([], $empty_discovery->getDefinitions()); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/CustomPlugin.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/CustomPlugin.php new file mode 100644 index 0000000000000000000000000000000000000000..9100fb800d9944934121c8693459a39b86fff50d --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/CustomPlugin.php @@ -0,0 +1,32 @@ +<?php + +namespace com\example\PluginNamespace; + +use Drupal\Component\Plugin\Attribute\Plugin; + +/** + * Custom plugin attribute. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class CustomPlugin extends Plugin { + + /** + * Constructs a CustomPlugin attribute object. + * + * @param string $id + * The attribute class ID. + * @param string $title + * The title. + */ + public function __construct( + public readonly string $id, + public readonly string $title + ) {} + +} + +/** + * Custom plugin attribute. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class CustomPlugin2 extends Plugin {} diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php new file mode 100644 index 0000000000000000000000000000000000000000..d550e99303e94e0ed768797ad6a2efcc77c93f84 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php @@ -0,0 +1,17 @@ +<?php + +namespace com\example\PluginNamespace; + +use Drupal\Component\Plugin\Attribute\Plugin; + +/** + * Provides a custom test plugin. + */ +#[Plugin( + id: "discovery_test_1", +)] +#[CustomPlugin( + id: "discovery_test_1", + title: "Discovery test plugin" +)] +class AttributeDiscoveryTest1 {} diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/PluginIdTest.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/PluginIdTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8c8391228330c4e1515e44b4888d17fdc246d0fc --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/PluginIdTest.php @@ -0,0 +1,38 @@ +<?php + +namespace Drupal\Tests\Component\Plugin\Attribute; + +use Drupal\Component\Plugin\Attribute\PluginID; +use PHPUnit\Framework\TestCase; + +/** + * @coversDefaultClass \Drupal\Component\Plugin\Attribute\PluginId + * @group Attribute + */ +class PluginIdTest extends TestCase { + + /** + * @covers ::get + */ + public function testGet() { + // Assert plugin starts with only an ID. + $plugin = new PluginID(id: 'test'); + // Plugin's always have a class set by discovery. + $plugin->setClass('bar'); + $this->assertEquals([ + 'id' => 'test', + 'class' => 'bar', + 'provider' => NULL, + ], $plugin->get()); + + // Set values and ensure we can retrieve them. + $plugin->setClass('bar2'); + $plugin->setProvider('baz'); + $this->assertEquals([ + 'id' => 'test', + 'class' => 'bar2', + 'provider' => 'baz', + ], $plugin->get()); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/PluginTest.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/PluginTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7fdfaa1eb90327ed6660f19e1e0ddb93cceff76e --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/PluginTest.php @@ -0,0 +1,63 @@ +<?php + +namespace Drupal\Tests\Component\Plugin\Attribute; + +use Drupal\Component\Plugin\Attribute\Plugin; +use PHPUnit\Framework\TestCase; + +/** + * @coversDefaultClass \Drupal\Component\Annotation\Plugin + * @group Attribute + */ +class PluginTest extends TestCase { + + /** + * @covers ::__construct + * @covers ::get + */ + public function testGet() { + $plugin = new PluginStub(id: 'example', deriver: 'test'); + $plugin->setClass('foo'); + $this->assertEquals([ + 'id' => 'example', + 'class' => 'foo', + 'deriver' => 'test', + ], $plugin->get()); + } + + /** + * @covers ::setProvider + * @covers ::getProvider + */ + public function testSetProvider() { + $plugin = new Plugin(id: 'example'); + $plugin->setProvider('example'); + $this->assertEquals('example', $plugin->getProvider()); + } + + /** + * @covers ::getId + */ + public function testGetId() { + $plugin = new Plugin(id: 'example'); + $this->assertEquals('example', $plugin->getId()); + } + + /** + * @covers ::setClass + * @covers ::getClass + */ + public function testSetClass() { + $plugin = new Plugin(id: 'test'); + $plugin->setClass('example'); + $this->assertEquals('example', $plugin->getClass()); + } + +} + +/** + * {@inheritdoc} + */ +class PluginStub extends Plugin { + +} diff --git a/core/tests/Drupal/Tests/Component/Plugin/Discovery/AttributeBridgeDecoratorTest.php b/core/tests/Drupal/Tests/Component/Plugin/Discovery/AttributeBridgeDecoratorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f6b37647a498d1901041139e7bd0a60a0a45e154 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Plugin/Discovery/AttributeBridgeDecoratorTest.php @@ -0,0 +1,105 @@ +<?php + +namespace Drupal\Tests\Component\Plugin\Discovery; + +use Drupal\Component\Plugin\Attribute\Plugin; +use Drupal\Component\Plugin\Definition\PluginDefinition; +use Drupal\Component\Plugin\Discovery\AttributeBridgeDecorator; +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use PHPUnit\Framework\TestCase; + +/** + * @coversDefaultClass \Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator + * @group Plugin + */ +class AttributeBridgeDecoratorTest extends TestCase { + + /** + * @covers ::getDefinitions + */ + public function testGetDefinitions() { + // Normally the attribute classes would be autoloaded. + include_once __DIR__ . '/../Attribute/Fixtures/CustomPlugin.php'; + include_once __DIR__ . '/../Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php'; + + $definitions = []; + $definitions['object'] = new ObjectDefinition(['id' => 'foo']); + $definitions['array'] = [ + 'id' => 'bar', + 'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1', + ]; + $discovery = $this->createMock(DiscoveryInterface::class); + $discovery->expects($this->any()) + ->method('getDefinitions') + ->willReturn($definitions); + + $decorator = new AttributeBridgeDecorator($discovery, TestAttribute::class); + + $expected = [ + 'object' => new ObjectDefinition(['id' => 'foo']), + 'array' => (new ObjectDefinition(['id' => 'bar']))->setClass('com\example\PluginNamespace\AttributeDiscoveryTest1'), + ]; + $this->assertEquals($expected, $decorator->getDefinitions()); + } + + /** + * Tests that the decorator of other methods works. + * + * @covers ::__call + */ + public function testOtherMethod(): void { + // Normally the attribute classes would be autoloaded. + include_once __DIR__ . '/../Attribute/Fixtures/CustomPlugin.php'; + include_once __DIR__ . '/../Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php'; + + $discovery = $this->createMock(ExtendedDiscoveryInterface::class); + $discovery->expects($this->exactly(2)) + ->method('otherMethod') + ->willReturnCallback(fn($id) => $id === 'foo'); + + $decorator = new AttributeBridgeDecorator($discovery, TestAttribute::class); + + $this->assertTrue($decorator->otherMethod('foo')); + $this->assertFalse($decorator->otherMethod('bar')); + } + +} + +interface ExtendedDiscoveryInterface extends DiscoveryInterface { + + public function otherMethod(string $id): bool; + +} + +/** + * {@inheritdoc} + */ +class TestAttribute extends Plugin { + + /** + * {@inheritdoc} + */ + public function get(): object { + return new ObjectDefinition(parent::get()); + } + +} + +/** + * {@inheritdoc} + */ +class ObjectDefinition extends PluginDefinition { + + /** + * ObjectDefinition constructor. + * + * @param array $definition + * An array of definition values. + */ + public function __construct(array $definition) { + foreach ($definition as $property => $value) { + $this->{$property} = $value; + } + } + +}