class ComponentPluginManager
Same name in this branch
- 10 core/lib/Drupal/Core/Theme/ComponentPluginManager.php \Drupal\Core\Theme\ComponentPluginManager
Same name in other branches
- 11.x core/modules/sdc/src/ComponentPluginManager.php \Drupal\sdc\ComponentPluginManager
- 11.x core/lib/Drupal/Core/Theme/ComponentPluginManager.php \Drupal\Core\Theme\ComponentPluginManager
Defines a plugin manager to deal with sdc.
Modules and themes can create components by adding a folder under MODULENAME/components/my-component/my-component.sdc.yml.
@internal
Hierarchy
- class \Drupal\Component\Plugin\PluginManagerBase implements \Drupal\Component\Plugin\PluginManagerInterface uses \Drupal\Component\Plugin\Discovery\DiscoveryTrait
- class \Drupal\Core\Plugin\DefaultPluginManager extends \Drupal\Component\Plugin\PluginManagerBase implements \Drupal\Component\Plugin\PluginManagerInterface, \Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface, \Drupal\Core\Cache\CacheableDependencyInterface uses \Drupal\Component\Plugin\Discovery\DiscoveryCachedTrait, \Drupal\Core\Cache\UseCacheBackendTrait
- class \Drupal\sdc\ComponentPluginManager extends \Drupal\Core\Plugin\DefaultPluginManager
- class \Drupal\Core\Plugin\DefaultPluginManager extends \Drupal\Component\Plugin\PluginManagerBase implements \Drupal\Component\Plugin\PluginManagerInterface, \Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface, \Drupal\Core\Cache\CacheableDependencyInterface uses \Drupal\Component\Plugin\Discovery\DiscoveryCachedTrait, \Drupal\Core\Cache\UseCacheBackendTrait
Expanded class hierarchy of ComponentPluginManager
See also
5 files declare their use of ComponentPluginManager
- ComponentKernelTestBase.php in core/
modules/ sdc/ tests/ src/ Kernel/ ComponentKernelTestBase.php - ComponentNodeVisitor.php in core/
modules/ sdc/ src/ Twig/ ComponentNodeVisitor.php - ComponentRenderTest.php in core/
modules/ sdc/ tests/ src/ Kernel/ ComponentRenderTest.php - TwigComponentLoader.php in core/
modules/ sdc/ src/ Twig/ TwigComponentLoader.php - TwigExtension.php in core/
modules/ sdc/ src/ Twig/ TwigExtension.php
File
-
core/
modules/ sdc/ src/ ComponentPluginManager.php, line 34
Namespace
Drupal\sdcView source
final class ComponentPluginManager extends DefaultPluginManager {
/**
* {@inheritdoc}
*/
protected $defaults = [
'class' => Component::class,
];
/**
* Constructs SdcPluginManager object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $themeHandler
* The theme handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend
* Cache backend instance to use.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The configuration factory.
* @param \Drupal\Core\Theme\ThemeManagerInterface $themeManager
* The theme manager.
* @param \Drupal\sdc\ComponentNegotiator $componentNegotiator
* The component negotiator.
* @param \Drupal\Core\File\FileSystemInterface $fileSystem
* The file system service.
* @param \Drupal\sdc\Component\SchemaCompatibilityChecker $compatibilityChecker
* The compatibility checker.
* @param \Drupal\sdc\Component\ComponentValidator $componentValidator
* The component validator.
* @param string $appRoot
* The application root.
*/
public function __construct(ModuleHandlerInterface $module_handler, ThemeHandlerInterface $themeHandler, CacheBackendInterface $cacheBackend, ConfigFactoryInterface $configFactory, ThemeManagerInterface $themeManager, ComponentNegotiator $componentNegotiator, FileSystemInterface $fileSystem, SchemaCompatibilityChecker $compatibilityChecker, ComponentValidator $componentValidator, string $appRoot) {
// We are skipping the call to the parent constructor to avoid initializing
// variables aimed for annotation discovery, that are unnecessary here.
// Plugin managers using YAML discovery also skip the parent constructor,
// like LinkRelationTypeManager.
$this->moduleHandler = $module_handler;
$this->factory = new ContainerFactory($this);
$this->setCacheBackend($cacheBackend, 'sdc_plugins');
// Note that we are intentionally skipping $this->alterInfo('sdc_info');
// We want to ensure that everything related to a component is in the
// single directory. If the alteration of a component is necessary,
// component replacement is the preferred tool for that.
}
/**
* Creates an instance.
*
* @throws \Drupal\sdc\Exception\ComponentNotFoundException
*
* @internal
*/
public function createInstance($plugin_id, array $configuration = []) : Component {
$configuration['app_root'] = $this->appRoot;
$configuration['enforce_schemas'] = $this->shouldEnforceSchemas($this->definitions[$plugin_id] ?? []);
try {
$instance = parent::createInstance($plugin_id, $configuration);
if (!$instance instanceof Component) {
throw new ComponentNotFoundException(sprintf('Unable to find component "%s" in the component repository.', $plugin_id));
}
return $instance;
} catch (PluginException $e) {
// Cast the PluginNotFound to a more specific exception.
$message = sprintf('Unable to find component "%s" in the component repository. [%s]', $plugin_id, $e->getMessage());
throw new ComponentNotFoundException($message, $e->getCode(), $e);
}
}
/**
* Gets a component for rendering.
*
* @param string $component_id
* The component ID.
*
* @return \Drupal\sdc\Plugin\Component
* The component.
*
* @throws \Drupal\sdc\Exception\ComponentNotFoundException
*
* @internal
*/
public function find(string $component_id) : Component {
$definitions = $this->getDefinitions();
if (empty($definitions)) {
throw new ComponentNotFoundException('Unable to find any component definition.');
}
$negotiated_plugin_id = $this->componentNegotiator
->negotiate($component_id, $definitions);
return $this->createInstance($negotiated_plugin_id ?? $component_id);
}
/**
* Gets all components.
*
* @return \Drupal\sdc\Plugin\Component[]
* An array of Component objects.
*
* @internal
*/
public function getAllComponents() : array {
$plugin_ids = array_keys($this->getDefinitions());
return array_values(array_filter(array_map([
$this,
'createInstance',
], $plugin_ids)));
}
/**
* {@inheritdoc}
*/
public function clearCachedDefinitions() : void {
parent::clearCachedDefinitions();
$this->componentNegotiator
->clearCache();
}
/**
* Creates the library declaration array from a component definition.
*
* @param array $definition
* The component definition.
*
* @return array
* The library for the Library API.
*/
protected function libraryFromDefinition(array $definition) : array {
$metadata_path = $definition[YamlDirectoryDiscovery::FILE_KEY];
$component_directory = $this->fileSystem
->dirname($metadata_path);
// Add the JS and CSS files.
$library = [];
$css_file = $this->findAsset($component_directory, $definition['machineName'], 'css', TRUE);
if ($css_file) {
$library['css']['component'][$css_file] = [];
}
$js_file = $this->findAsset($component_directory, $definition['machineName'], 'js', TRUE);
if ($js_file) {
$library['js'][$js_file] = [];
}
// We allow component authors to use library overrides to use files relative
// to the component directory. So we need to fix the paths here.
if (!empty($definition['libraryOverrides'])) {
$overrides = $this->translateLibraryPaths($definition['libraryOverrides'], $component_directory);
// Apply library overrides.
$library = array_merge($library, $overrides);
// Ensure that 'core/drupal' is always a dependency. This will ensure that
// JS behaviors are attached.
$library['dependencies'][] = 'core/drupal';
$library['dependencies'] = array_unique($library['dependencies']);
}
return $library;
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() : DirectoryWithMetadataPluginDiscovery {
if (!isset($this->discovery)) {
$directories = $this->getScanDirectories();
$this->discovery = new DirectoryWithMetadataPluginDiscovery($directories, 'sdc', $this->fileSystem);
}
return $this->discovery;
}
/**
* {@inheritdoc}
*/
protected function providerExists($provider) {
return $this->moduleHandler
->moduleExists($provider) || $this->themeHandler
->themeExists($provider);
}
/**
* {@inheritdoc}
*/
protected function alterDefinitions(&$definitions) {
// Save in the definition whether this is a module or a theme. This is
// important because when creating the plugin instance (the Component
// object) we'll need to negotiate based on the active theme.
$definitions = array_map([
$this,
'alterDefinition',
], $definitions);
// Validate the definition after alterations.
assert(Inspector::assertAll(fn(array $definition) => $this->isValidDefinition($definition), $definitions));
parent::alterDefinitions($definitions);
// Finally, validate replacements.
$replacing_definitions = array_filter($definitions, static fn(array $definition) => ($definition['replaces'] ?? NULL) && ($definitions[$definition['replaces']] ?? NULL));
$validation_errors = array_reduce($replacing_definitions, function (array $errors, array $new_definition) use ($definitions) {
$original_definition = $definitions[$new_definition['replaces']];
$original_schemas = $original_definition['props'] ?? NULL;
$new_schemas = $new_definition['props'] ?? NULL;
if (!$original_schemas || !$new_schemas) {
return [
sprintf("Component \"%s\" is attempting to replace \"%s\", however component replacement requires both components to have schema definitions.", $new_definition['id'], $original_definition['id']),
];
}
try {
$this->compatibilityChecker
->isCompatible($original_schemas, $new_schemas);
} catch (IncompatibleComponentSchema $e) {
$errors[] = sprintf("\"%s\" is incompatible with the component is wants to replace \"%s\". Errors:\n%s", $new_definition['id'], $original_definition['id'], $e->getMessage());
}
return $errors;
}, []);
if (!empty($validation_errors)) {
throw new IncompatibleComponentSchema(implode("\n", $validation_errors));
}
}
/**
* Alters the plugin definition with computed properties.
*
* @param array $definition
* The definition.
*
* @return array
* The altered definition.
*/
protected function alterDefinition(array $definition) : array {
$definition['extension_type'] = $this->moduleHandler
->moduleExists($definition['provider']) ? ExtensionType::Module : ExtensionType::Theme;
$metadata_path = $definition[YamlDirectoryDiscovery::FILE_KEY];
$component_directory = $this->fileSystem
->dirname($metadata_path);
$definition['path'] = $component_directory;
[
,
$machine_name,
] = explode(':', $definition['id']);
$definition['machineName'] = $machine_name;
$definition['library'] = $this->libraryFromDefinition($definition);
// Discover the template.
$template = $this->findAsset($component_directory, $definition['machineName'], 'twig');
$definition['template'] = basename($template);
$definition['documentation'] = 'No documentation found. Add a README.md in your component directory.';
$documentation_path = sprintf('%s/README.md', $this->fileSystem
->dirname($metadata_path));
if (file_exists($documentation_path)) {
$definition['documentation'] = file_get_contents($documentation_path);
}
return $definition;
}
/**
* Validates the metadata info.
*
* @param array $definition
* The component definition.
*
* @return bool
* TRUE if it's valid.
*
* @throws \Drupal\sdc\Exception\InvalidComponentException
*/
private function isValidDefinition(array $definition) : bool {
return $this->componentValidator
->validateDefinition($definition, $this->shouldEnforceSchemas($definition));
}
/**
* Get the list of directories to scan.
*
* @return string[]
* The directories.
*/
private function getScanDirectories() : array {
$extension_directories = [
$this->moduleHandler
->getModuleDirectories(),
$this->themeHandler
->getThemeDirectories(),
];
return array_map(static fn(string $path) => rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'components', $extension_directories);
}
/**
* Changes the library paths, so they can be used by the library system.
*
* We need this so we can let users apply overrides to JS and CSS files with
* paths relative to the component.
*
* @param array $overrides
* The library overrides as provided by the component author.
* @param string $component_directory
* The directory for the component.
*
* @return array
* The overrides with the fixed paths.
*/
private function translateLibraryPaths(array $overrides, string $component_directory) : array {
// We only alter the keys of the CSS and JS entries.
$altered_overrides = $overrides;
unset($altered_overrides['css'], $altered_overrides['js']);
$css = $overrides['css'] ?? [];
$js = $overrides['js'] ?? [];
foreach ($css as $dir => $css_info) {
foreach ($css_info as $filename => $options) {
if (!UrlHelper::isExternal($filename)) {
$absolute_filename = sprintf('%s%s%s', $component_directory, DIRECTORY_SEPARATOR, $filename);
$altered_filename = $this->makePathRelativeToLibraryRoot($absolute_filename);
$altered_overrides['css'][$dir][$altered_filename] = $options;
}
else {
$altered_overrides['css'][$dir][$filename] = $options;
}
}
}
foreach ($js as $filename => $options) {
if (!UrlHelper::isExternal($filename)) {
$absolute_filename = sprintf('%s%s%s', $component_directory, DIRECTORY_SEPARATOR, $filename);
$altered_filename = $this->makePathRelativeToLibraryRoot($absolute_filename);
$altered_overrides['js'][$altered_filename] = $options;
}
else {
$altered_overrides['js'][$filename] = $options;
}
}
return $altered_overrides;
}
/**
* Assess whether schemas are mandatory for props.
*
* Schemas are always mandatory for component provided by modules. It depends
* on a theme setting for theme components.
*
* @param array $definition
* The plugin definition.
*
* @return bool
* TRUE if schemas are mandatory.
*/
private function shouldEnforceSchemas(array $definition) : bool {
$provider_type = $definition['extension_type'] ?? NULL;
if ($provider_type !== ExtensionType::Theme) {
return TRUE;
}
return $this->themeHandler
->getTheme($definition['provider'])?->info['enforce_prop_schemas'] ?? FALSE;
}
/**
* Finds assets related to the provided metadata file.
*
* @param string $component_directory
* The component directory for the plugin.
* @param string $machine_name
* The component's machine name.
* @param string $file_extension
* The file extension to detect.
* @param bool $make_relative
* TRUE to make the filename relative to the SDC module location.
*
* @return string|null
* Filenames, maybe relative to the sdc module.
*/
private function findAsset(string $component_directory, string $machine_name, string $file_extension, bool $make_relative = FALSE) : ?string {
$absolute_path = sprintf('%s%s%s.%s', $component_directory, DIRECTORY_SEPARATOR, $machine_name, $file_extension);
if (!file_exists($absolute_path)) {
return NULL;
}
return $make_relative ? $this->makePathRelativeToLibraryRoot($absolute_path) : $absolute_path;
}
/**
* Takes a path and makes it relative to the library provider.
*
* Drupal will take a path relative to the library provider in order to put
* CSS and JS in the HTML page. The SDC module is the provider for all the
* auto-generated libraries for the components. This means that in order to
* add <root>/themes/custom/my_theme/components/my-component/my-component.css
* in the page, we need to crawl back up from <root>/core/modules/sdc first:
* ../../../../themes/custom/my_theme/components/my-component/my-component.css
*
* @param string $path
* The path to the file.
*
* @return string
* The path relative to the library provider root.
*/
private function makePathRelativeToLibraryRoot(string $path) : string {
$library_provider_root = $this->moduleHandler
->getModule('sdc')
->getPath();
$num_dots = count(array_filter(explode(DIRECTORY_SEPARATOR, $library_provider_root)));
$dots = str_repeat('../', $num_dots);
$path_from_root = str_starts_with($path, $this->appRoot) ? substr($path, strlen($this->appRoot) + 1) : $path;
return $dots . $path_from_root;
}
}
Members
Title Sort descending | Modifiers | Object type | Summary | Overriden Title | Overrides |
---|---|---|---|---|---|
ComponentPluginManager::$defaults | protected | property | A set of defaults to be referenced by $this->processDefinition(). | Overrides DefaultPluginManager::$defaults | |
ComponentPluginManager::alterDefinition | protected | function | Alters the plugin definition with computed properties. | ||
ComponentPluginManager::alterDefinitions | protected | function | Invokes the hook to alter the definitions if the alter hook is set. | Overrides DefaultPluginManager::alterDefinitions | |
ComponentPluginManager::clearCachedDefinitions | public | function | Clears static and persistent plugin definition caches. | Overrides DefaultPluginManager::clearCachedDefinitions | |
ComponentPluginManager::createInstance | public | function | Creates an instance. | Overrides PluginManagerBase::createInstance | |
ComponentPluginManager::find | public | function | Gets a component for rendering. | ||
ComponentPluginManager::findAsset | private | function | Finds assets related to the provided metadata file. | ||
ComponentPluginManager::getAllComponents | public | function | Gets all components. | ||
ComponentPluginManager::getDiscovery | protected | function | Gets the plugin discovery. | Overrides DefaultPluginManager::getDiscovery | |
ComponentPluginManager::getScanDirectories | private | function | Get the list of directories to scan. | ||
ComponentPluginManager::isValidDefinition | private | function | Validates the metadata info. | ||
ComponentPluginManager::libraryFromDefinition | protected | function | Creates the library declaration array from a component definition. | ||
ComponentPluginManager::makePathRelativeToLibraryRoot | private | function | Takes a path and makes it relative to the library provider. | ||
ComponentPluginManager::providerExists | protected | function | Determines if the provider of a definition exists. | Overrides DefaultPluginManager::providerExists | |
ComponentPluginManager::shouldEnforceSchemas | private | function | Assess whether schemas are mandatory for props. | ||
ComponentPluginManager::translateLibraryPaths | private | function | Changes the library paths, so they can be used by the library system. | ||
ComponentPluginManager::__construct | public | function | Constructs SdcPluginManager object. | Overrides DefaultPluginManager::__construct | |
DefaultPluginManager::$additionalAnnotationNamespaces | protected | property | Additional annotation namespaces. | ||
DefaultPluginManager::$alterHook | protected | property | Name of the alter hook if one should be invoked. | ||
DefaultPluginManager::$cacheKey | protected | property | The cache key. | ||
DefaultPluginManager::$cacheTags | protected | property | An array of cache tags to use for the cached definitions. | ||
DefaultPluginManager::$moduleExtensionList | protected | property | The module extension list. | ||
DefaultPluginManager::$moduleHandler | protected | property | The module handler to invoke the alter hook. | 1 | |
DefaultPluginManager::$namespaces | protected | property | An object of root paths that are traversable. | ||
DefaultPluginManager::$pluginDefinitionAnnotationName | protected | property | The name of the annotation that contains the plugin definition. | ||
DefaultPluginManager::$pluginDefinitionAttributeName | protected | property | The name of the attribute that contains the plugin definition. | ||
DefaultPluginManager::$pluginInterface | protected | property | The interface each plugin should implement. | 1 | |
DefaultPluginManager::$subdir | protected | property | The subdirectory within a namespace to look for plugins. | ||
DefaultPluginManager::alterInfo | protected | function | Sets the alter hook name. | ||
DefaultPluginManager::extractProviderFromDefinition | protected | function | Extracts the provider from a plugin definition. | ||
DefaultPluginManager::findDefinitions | protected | function | Finds plugin definitions. | 7 | |
DefaultPluginManager::getCacheContexts | public | function | The cache contexts associated with this object. | Overrides CacheableDependencyInterface::getCacheContexts | |
DefaultPluginManager::getCachedDefinitions | protected | function | Returns the cached plugin definitions of the decorated discovery class. | ||
DefaultPluginManager::getCacheMaxAge | public | function | The maximum age for which this object may be cached. | Overrides CacheableDependencyInterface::getCacheMaxAge | |
DefaultPluginManager::getCacheTags | public | function | The cache tags associated with this object. | Overrides CacheableDependencyInterface::getCacheTags | |
DefaultPluginManager::getDefinitions | public | function | Gets the definition of all plugins for this type. | Overrides DiscoveryTrait::getDefinitions | 2 |
DefaultPluginManager::getFactory | protected | function | Gets the plugin factory. | Overrides PluginManagerBase::getFactory | |
DefaultPluginManager::processDefinition | public | function | Performs extra processing on plugin definitions. | 14 | |
DefaultPluginManager::setCacheBackend | public | function | Initialize the cache backend. | ||
DefaultPluginManager::setCachedDefinitions | protected | function | Sets a cache of plugin definitions for the decorated discovery class. | ||
DefaultPluginManager::useCaches | public | function | Disable the use of caches. | Overrides CachedDiscoveryInterface::useCaches | 1 |
DiscoveryCachedTrait::$definitions | protected | property | Cached definitions array. | 1 | |
DiscoveryCachedTrait::getDefinition | public | function | Overrides DiscoveryTrait::getDefinition | 3 | |
DiscoveryTrait::doGetDefinition | protected | function | Gets a specific plugin definition. | ||
DiscoveryTrait::hasDefinition | public | function | |||
PluginManagerBase::$discovery | protected | property | The object that discovers plugins managed by this manager. | ||
PluginManagerBase::$factory | protected | property | The object that instantiates plugins managed by this manager. | ||
PluginManagerBase::$mapper | protected | property | The object that returns the preconfigured plugin instance appropriate for a particular runtime condition. | ||
PluginManagerBase::getFallbackPluginId | protected | function | Gets a fallback id for a missing plugin. | 6 | |
PluginManagerBase::getInstance | public | function | 6 | ||
PluginManagerBase::handlePluginNotFound | protected | function | Allows plugin managers to specify custom behavior if a plugin is not found. | 1 | |
UseCacheBackendTrait::$cacheBackend | protected | property | Cache backend instance. | ||
UseCacheBackendTrait::$useCaches | protected | property | Flag whether caches should be used or skipped. | ||
UseCacheBackendTrait::cacheGet | protected | function | Fetches from the cache backend, respecting the use caches flag. | ||
UseCacheBackendTrait::cacheSet | protected | function | Stores data in the persistent cache, respecting the use caches flag. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.