class AttributeClassDiscovery

Same name in this branch
  1. 10 core/lib/Drupal/Core/Plugin/Discovery/AttributeClassDiscovery.php \Drupal\Core\Plugin\Discovery\AttributeClassDiscovery
Same name and namespace in other branches
  1. 11.x core/lib/Drupal/Core/Plugin/Discovery/AttributeClassDiscovery.php \Drupal\Core\Plugin\Discovery\AttributeClassDiscovery
  2. 11.x core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php \Drupal\Component\Plugin\Discovery\AttributeClassDiscovery

Defines a discovery mechanism to find plugins with attributes.

Hierarchy

Expanded class hierarchy of AttributeClassDiscovery

3 files declare their use of AttributeClassDiscovery
AttributeClassDiscovery.php in core/lib/Drupal/Core/Plugin/Discovery/AttributeClassDiscovery.php
AttributeClassDiscoveryCachedTest.php in core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php
AttributeClassDiscoveryTest.php in core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryTest.php

File

core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php, line 13

Namespace

Drupal\Component\Plugin\Discovery
View source
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');
              try {
                [
                  '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 that the file is not parsed again.
                  $this->fileCache
                    ->set($fileinfo->getPathName(), [
                    NULL,
                  ]);
                }
              } catch (\Error $e) {
                if (!preg_match('/(Class|Interface) .* not found$/', $e->getMessage())) {
                  throw $e;
                }
              }
            }
          }
        }
      }
    }
    // 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
   * @throws \Error
   */
  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;
  }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title Overrides
AttributeClassDiscovery::$fileCache protected property The file cache object.
AttributeClassDiscovery::getDefinitions public function Overrides DiscoveryTrait::getDefinitions 1
AttributeClassDiscovery::getFileCacheSuffix protected function Gets the file cache suffix. 1
AttributeClassDiscovery::getPluginNamespaces protected function Gets an array of PSR-4 namespaces to search for plugin classes. 1
AttributeClassDiscovery::parseClass protected function Parses attributes from a class. 1
AttributeClassDiscovery::prepareAttributeDefinition protected function Prepares the attribute definition. 1
AttributeClassDiscovery::__construct public function Constructs a new instance. 1
DiscoveryTrait::doGetDefinition protected function Gets a specific plugin definition.
DiscoveryTrait::getDefinition public function 3
DiscoveryTrait::hasDefinition public function

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.