countdown-8.x-1.8/src/CountdownLibraryPluginManager.php
src/CountdownLibraryPluginManager.php
<?php
declare(strict_types=1);
namespace Drupal\countdown;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\countdown\Annotation\CountdownLibrary;
use Drupal\countdown\Plugin\CountdownLibraryPluginInterface;
use Psr\Log\LoggerInterface;
/**
* @file
* Provides the plugin manager for Countdown Library plugins.
*/
/**
* Plugin manager for Countdown Library plugins.
*
* The plugin manager discovers, instantiates, and manages countdown library
* plugins. It offers helpers for querying, validation, and building library
* definitions used by Drupal's asset system.
*
* @see CountdownLibrary
* @see CountdownLibraryPluginInterface
* @see \Drupal\countdown\Plugin\CountdownLibraryPluginBase
* @see plugin_api
*/
class CountdownLibraryPluginManager extends DefaultPluginManager {
use StringTranslationTrait;
/**
* Channel logger for this manager.
*
* @var \Psr\Log\LoggerInterface
*/
protected LoggerInterface $logger;
/**
* Constructs the plugin manager.
*
* The manager is registered as a service, so all dependencies are injected.
* This avoids static calls and improves testability.
*
* @param \Traversable $namespaces
* A traversable of PSR-4 namespaces to search for plugin classes.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend for plugin definitions.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to support alter hooks.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger channel factory.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation service for human-readable strings.
*/
public function __construct(
\Traversable $namespaces,
CacheBackendInterface $cache_backend,
ModuleHandlerInterface $module_handler,
LoggerChannelFactoryInterface $logger_factory,
TranslationInterface $string_translation,
) {
// Register the base plugin directory, interface, and annotation class.
parent::__construct(
'Plugin/CountdownLibrary',
$namespaces,
$module_handler,
CountdownLibraryPluginInterface::class,
CountdownLibrary::class
);
// Allow other modules to alter definitions.
$this->alterInfo('countdown_library_info');
// Cache plugin definitions for performance.
$this->setCacheBackend($cache_backend, 'countdown_library_plugins');
// Initialize services.
$this->logger = $logger_factory->get('countdown');
$this->stringTranslation = $string_translation;
}
/**
* Gets all plugin instances, optionally filtering by installation state.
*
* @param bool $only_installed
* If TRUE, only libraries that are installed will be returned.
*
* @return \Drupal\countdown\Plugin\CountdownLibraryPluginInterface[]
* An array of plugin instances keyed by plugin ID.
*/
public function getAllPlugins(bool $only_installed = FALSE): array {
$plugins = [];
foreach ($this->getDefinitions() as $plugin_id => $definition) {
try {
/** @var \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin */
$plugin = $this->createInstance($plugin_id);
if (!$only_installed || $plugin->isInstalled()) {
$plugins[$plugin_id] = $plugin;
}
}
catch (\Throwable $e) {
// Log and continue so one bad plugin does not block others.
$this->logger->error(
'Failed to create countdown plugin @id: @message',
[
'@id' => $plugin_id,
'@message' => $e->getMessage(),
]
);
}
}
// Sort by weight for stable UI ordering.
uasort($plugins, static function ($a, $b): int {
return $a->getWeight() <=> $b->getWeight();
});
return $plugins;
}
/**
* Gets installed plugin instances.
*
* @return \Drupal\countdown\Plugin\CountdownLibraryPluginInterface[]
* An array of installed plugin instances keyed by plugin ID.
*/
public function getInstalledPlugins(): array {
return $this->getAllPlugins(TRUE);
}
/**
* Gets a specific plugin instance by ID.
*
* @param string $plugin_id
* The plugin ID.
*
* @return \Drupal\countdown\Plugin\CountdownLibraryPluginInterface|null
* The plugin instance or NULL if it could not be created.
*/
public function getPlugin(string $plugin_id): ?CountdownLibraryPluginInterface {
try {
/** @var \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin */
$plugin = $this->createInstance($plugin_id);
return $plugin;
}
catch (\Throwable $e) {
$this->logger->error(
'Failed to create countdown plugin @id: @message',
[
'@id' => $plugin_id,
'@message' => $e->getMessage(),
]
);
return NULL;
}
}
/**
* Builds select options for forms listing libraries.
*
* @param bool $only_installed
* If TRUE, only include installed libraries.
* @param bool $show_versions
* If TRUE, append version strings to labels.
* @param bool $group_by_type
* If TRUE, group by library type ("Core" vs "External").
*
* @return array
* A nested options array suitable for form API elements.
*/
public function getPluginOptions(
bool $only_installed = FALSE,
bool $show_versions = TRUE,
bool $group_by_type = FALSE,
): array {
$plugins = $this->getAllPlugins($only_installed);
$options = [];
foreach ($plugins as $plugin_id => $plugin) {
$label = $plugin->getLabel();
if ($show_versions) {
// Prefer installed version; fall back to declared minimum version.
$version = $plugin->getInstalledVersion() ?: $plugin->getRequiredVersion();
if ($version) {
$label .= ' (v' . $version . ')';
}
}
if ($plugin->isExperimental()) {
$label .= ' [' . (string) $this->t('Experimental') . ']';
}
if ($group_by_type) {
$type = $plugin->getType();
$type_label = $type === 'core' ? $this->t('Core') : $this->t('External');
$options[(string) $type_label][$plugin_id] = $label;
}
else {
$options[$plugin_id] = $label;
}
}
return $options;
}
/**
* Gets status information for all plugins.
*
* @return array
* An array of status arrays keyed by plugin ID. Each entry contains the
* plugin instance under the 'plugin' key for convenience.
*/
public function getAllPluginStatuses(): array {
$statuses = [];
foreach ($this->getAllPlugins() as $plugin_id => $plugin) {
$statuses[$plugin_id] = $plugin->getStatus();
$statuses[$plugin_id]['plugin'] = $plugin;
}
return $statuses;
}
/**
* Validates if a given plugin can be used.
*
* @param string $plugin_id
* The plugin ID to validate.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup[]
* An array of validation messages. Empty when no issues are found.
*/
public function validatePlugin(string $plugin_id): array {
$messages = [];
$plugin = $this->getPlugin($plugin_id);
if (!$plugin) {
$messages[] = $this->t('Plugin @id does not exist.', ['@id' => $plugin_id]);
return $messages;
}
if (!$plugin->isInstalled()) {
$messages[] = $this->t('Library @label is not installed.', [
'@label' => $plugin->getLabel(),
]);
}
elseif (!$plugin->versionMeetsRequirements()) {
$messages[] = $this->t('Library @label does not meet version requirements.', [
'@label' => $plugin->getLabel(),
]);
}
return $messages;
}
/**
* Gets plugins that support CDN loading.
*
* @return \Drupal\countdown\Plugin\CountdownLibraryPluginInterface[]
* An array of CDN-capable plugins keyed by plugin ID.
*/
public function getCdnCapablePlugins(): array {
$plugins = [];
foreach ($this->getAllPlugins() as $plugin_id => $plugin) {
if ($plugin->getCdnConfig() !== NULL) {
$plugins[$plugin_id] = $plugin;
}
}
return $plugins;
}
/**
* Clears manager and plugin-level caches.
*
* This clears cached plugin definitions and asks each plugin instance to
* reset its internal caches to reflect file system changes.
*/
public function resetAllCaches(): void {
$this->clearCachedDefinitions();
foreach ($this->getAllPlugins() as $plugin) {
$plugin->resetCache();
}
}
/**
* Gets plugin definitions filtered by type.
*
* @param string $type
* The library type. Usually 'core' or 'external'.
*
* @return array
* An array of raw plugin definitions keyed by plugin ID.
*/
public function getDefinitionsByType(string $type): array {
$definitions = [];
foreach ($this->getDefinitions() as $plugin_id => $definition) {
if (($definition['type'] ?? 'external') === $type) {
$definitions[$plugin_id] = $definition;
}
}
return $definitions;
}
/**
* Checks if a plugin exists.
*
* @param string $plugin_id
* The plugin ID.
*
* @return bool
* TRUE if the plugin exists, FALSE otherwise.
*/
public function hasPlugin(string $plugin_id): bool {
return $this->hasDefinition($plugin_id);
}
/**
* Gets installed plugins that meet their version requirements.
*
* @return \Drupal\countdown\Plugin\CountdownLibraryPluginInterface[]
* An array of compatible plugin instances keyed by plugin ID.
*/
public function getCompatiblePlugins(): array {
$plugins = [];
foreach ($this->getInstalledPlugins() as $plugin_id => $plugin) {
if ($plugin->versionMeetsRequirements()) {
$plugins[$plugin_id] = $plugin;
}
}
return $plugins;
}
/**
* Builds library definitions for hook_library_info_build().
*
* @param bool $minified
* Whether to use minified assets when available.
*
* @return array
* An array of library definitions keyed by a machine name. The caller is
* responsible for prefixing with the 'countdown.' or similar schema.
*/
public function buildLibraryDefinitions(bool $minified = TRUE): array {
$definitions = [];
foreach ($this->getInstalledPlugins() as $plugin_id => $plugin) {
$lib_name = 'countdown.' . $plugin_id;
$definitions[$lib_name] = $plugin->buildLibraryDefinition($minified);
}
return $definitions;
}
}
