toolshed-8.x-1.x-dev/src/Discovery/AttributeDiscovery.php
src/Discovery/AttributeDiscovery.php
<?php
namespace Drupal\toolshed\Discovery;
use Drupal\Component\Discovery\DiscoverableInterface;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\FileCache\FileCacheInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\toolshed\Strategy\Attribute\StrategyInterface;
/**
* Discover strategies in modules by searching for strategy class attributes.
*
* The class attributes provide the strategy definition, and unlike the plugin
* discovery, the strategies are kept grouped by their providers. This allows
* better potential for a strategy manager to identify module vs theme provided
* strategies.
*/
class AttributeDiscovery implements DiscoverableInterface {
/**
* The filecache to store attribute discoveries.
*
* @var \Drupal\Component\FileCache\FileCacheInterface
*/
protected FileCacheInterface $fileCache;
/**
* The sub-directory to search for strategy class implementations.
*
* @var string
*/
protected string $subdir;
/**
* Create new instance of the strategy AttributeDiscovery classe.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module extension handler.
* @param string $subdir
* The sub-directory to search for strategy classes.
* @param class-string<\Drupal\toolshed\Strategy\Attribute\StrategyInterface> $attribute
* The attribute class which provides the strategy definition.
*/
public function __construct(protected ModuleHandlerInterface $moduleHandler, string $subdir, protected string $attribute) {
$this->subdir = trim($subdir, DIRECTORY_SEPARATOR);
$this->fileCache = FileCacheFactory::get('attribute_discovery:' . str_replace('/', '_', $attribute));
}
/**
* {@inheritdoc}
*/
public function findAll(): array {
$definitions = [];
$nsSuffix = str_replace(DIRECTORY_SEPARATOR, '\\', $this->subdir);
// Search for classes within all PSR-4 namespace locations.
foreach ($this->moduleHandler->getModuleList() as $name => $extension) {
$namespace = implode('\\', ['Drupal', $name, $nsSuffix]);
$dir = implode(DIRECTORY_SEPARATOR, [
'.' . base_path(), $extension->getPath(), 'src', $this->subdir,
]);
if (file_exists($dir)) {
$prefixOffset = strlen($dir);
$iter = new \RecursiveCallbackFilterIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
static fn($file) => 'php' === $file->getExtension()
);
foreach ($iter as $fileinfo) {
if ($cached = $this->fileCache->get($fileinfo->getPathName())) {
if (isset($cached['id'])) {
// Explicitly unserialize this to create a new object instance.
$definitions[$name][$cached['id']] = unserialize($cached['content'], [
'allowed_classes' => [
'\Stringable',
TranslatableMarkup::class,
],
]);
}
continue;
}
try {
$class = $namespace . str_replace(DIRECTORY_SEPARATOR, '\\', substr($fileinfo->getPath(), $prefixOffset));
$class .= '\\' . $fileinfo->getBasename('.php');
/** @var \Drupal\toolshed\Strategy\Attribute\StrategyInterface $attr */
$attr = $this->parseClass($class, $fileinfo);
if ($attr) {
$id = $attr->setProvider($name)->id();
$definitions[$name][$id] = $attr->get();
$this->fileCache->set($fileinfo->getPathName(), [
'id' => $id,
'content' => serialize($definitions[$name][$id]),
]);
}
else {
$this->fileCache->set($fileinfo->getPathName(), []);
}
}
catch (\Error $e) {
// Plugins may rely on Attribute classes defined by modules that
// are not installed. In such a case, a 'class not found' error
// may be thrown from reflection. Therefore silently skip over this
// class and avoid writing to the cache so that it can be detected
// when the dependencies are enabled.
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 \Drupal\toolshed\Strategy\Attribute\StrategyInterface|null
* 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): ?StrategyInterface {
$reflected = new \ReflectionClass($class);
if ($attributes = $reflected->getAttributes($this->attribute, \ReflectionAttribute::IS_INSTANCEOF)) {
/** @var \Drupal\toolshed\Strategy\Attribute\StrategyInterface $attribute */
$attribute = $attributes[0]->newInstance();
$attribute->setClass($class);
return $attribute;
}
return NULL;
}
}
