AttributeClassDiscovery.php

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

Namespace

Drupal\Component\Plugin\Discovery

File

core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php

View source
<?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(array $pluginNamespaces = [], 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;
    }

}

Classes

Title Deprecated Summary
AttributeClassDiscovery Defines a discovery mechanism to find plugins with attributes.

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