countdown-8.x-1.8/src/Service/CountdownLibraryManager.php
src/Service/CountdownLibraryManager.php
<?php
declare(strict_types=1);
namespace Drupal\countdown\Service;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\countdown\CountdownConstants;
use Drupal\countdown\CountdownLibraryPluginManager;
use Drupal\countdown\Utility\CdnUrlBuilder;
use Drupal\countdown\Utility\ConfigAccessor;
/**
* Service for managing countdown libraries using Plugin System.
*
* This service handles all countdown library operations including:
* - Library activation and switching.
* - Local and CDN loading methods.
* - Configuration management.
* - Validation and requirements checking.
* - Library definitions for Drupal's library system.
*/
class CountdownLibraryManager implements CountdownLibraryManagerInterface {
use StringTranslationTrait;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected ConfigFactoryInterface $configFactory;
/**
* The plugin manager.
*
* @var \Drupal\countdown\CountdownLibraryPluginManager
*/
protected CountdownLibraryPluginManager $pluginManager;
/**
* The library discovery service.
*
* @var \Drupal\countdown\Service\CountdownLibraryDiscoveryInterface
*/
protected CountdownLibraryDiscoveryInterface $libraryDiscovery;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected MessengerInterface $messenger;
/**
* The configuration accessor.
*
* @var \Drupal\countdown\Utility\ConfigAccessor
*/
protected ConfigAccessor $configAccessor;
/**
* The CDN URL builder.
*
* @var \Drupal\countdown\Utility\CdnUrlBuilder
*/
protected CdnUrlBuilder $cdnBuilder;
/**
* Static cache for library definitions.
*
* @var array
*/
protected array $libraryDefinitionsCache = [];
/**
* Static cache for CDN providers.
*
* @var array
*/
protected array $cdnProvidersCache = [];
/**
* Constructs a CountdownLibraryManager object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\countdown\CountdownLibraryPluginManager $plugin_manager
* The plugin manager.
* @param \Drupal\countdown\Service\CountdownLibraryDiscoveryInterface $library_discovery
* The library discovery service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(
ConfigFactoryInterface $config_factory,
CountdownLibraryPluginManager $plugin_manager,
CountdownLibraryDiscoveryInterface $library_discovery,
ModuleHandlerInterface $module_handler,
MessengerInterface $messenger,
TranslationInterface $string_translation,
) {
$this->configFactory = $config_factory;
$this->pluginManager = $plugin_manager;
$this->libraryDiscovery = $library_discovery;
$this->moduleHandler = $module_handler;
$this->messenger = $messenger;
$this->stringTranslation = $string_translation;
// Create utilities.
$this->configAccessor = new ConfigAccessor($config_factory);
$this->cdnBuilder = new CdnUrlBuilder();
}
/**
* {@inheritdoc}
*/
public function getActiveLibrary(): string {
$active = $this->configAccessor->getActiveLibrary();
$method = $this->getLoadingMethod();
// Validate the active library exists and is appropriate for the method.
if ($active && $this->pluginManager->hasPlugin($active)) {
$plugin = $this->pluginManager->getPlugin($active);
if ($plugin) {
// For local method, library must be installed.
if ($method === 'local' && $plugin->isInstalled()) {
return $active;
}
// For CDN method, library must support CDN.
if ($method === 'cdn' && $plugin->getCdnConfig() !== NULL) {
return $active;
}
}
}
// Fall back to first available library for the current method.
if ($method === 'local') {
$installed = $this->pluginManager->getInstalledPlugins();
if (!empty($installed)) {
return key($installed);
}
}
else {
$cdn_plugins = $this->pluginManager->getCdnCapablePlugins();
if (!empty($cdn_plugins)) {
return key($cdn_plugins);
}
}
// Final fallback to core library.
return CountdownConstants::DEFAULT_LIBRARY;
}
/**
* {@inheritdoc}
*/
public function setActiveLibrary(string $library_id): bool {
$plugin = $this->pluginManager->getPlugin($library_id);
if (!$plugin) {
throw new \InvalidArgumentException(
sprintf('Library "%s" does not exist.', $library_id)
);
}
$method = $this->getLoadingMethod();
// Validate based on loading method.
if ($method === 'local' && !$plugin->isInstalled()) {
throw new \InvalidArgumentException(
sprintf('Library "%s" is not installed. Install it or switch to CDN loading.', $library_id)
);
}
if ($method === 'cdn' && $plugin->getCdnConfig() === NULL) {
throw new \InvalidArgumentException(
sprintf('Library "%s" does not support CDN loading.', $library_id)
);
}
// Check version compatibility if local.
if ($method === 'local' && !$plugin->versionMeetsRequirements()) {
$status = $plugin->getStatus();
$this->messenger->addWarning($this->t('Library @library may not work correctly: @messages', [
'@library' => $plugin->getLabel(),
'@messages' => implode(' ', $status['messages']),
]));
}
// Save configuration.
$this->configFactory->getEditable('countdown.settings')
->set('library', $library_id)
->save();
// Clear caches to ensure new library is loaded.
$this->clearCache();
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getLoadingMethod(): string {
return $this->configAccessor->getLoadingMethod();
}
/**
* {@inheritdoc}
*/
public function setLoadingMethod(string $method): bool {
if (!in_array($method, ['local', 'cdn'])) {
throw new \InvalidArgumentException(
sprintf('Invalid loading method "%s". Must be "local" or "cdn".', $method)
);
}
$this->configFactory->getEditable('countdown.settings')
->set('method', $method)
->save();
// Clear caches.
$this->clearCache();
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getCdnProvider(): string {
return $this->configAccessor->getCdnProvider();
}
/**
* {@inheritdoc}
*/
public function setCdnProvider(string $provider): bool {
$valid_providers = $this->getAvailableCdnProviders();
if (!in_array($provider, $valid_providers)) {
throw new \InvalidArgumentException(
sprintf('Invalid CDN provider "%s". Valid providers are: %s',
$provider,
implode(', ', $valid_providers)
)
);
}
$this->configFactory->getEditable('countdown.settings')
->set('cdn.provider', $provider)
->save();
// Clear caches.
$this->clearCache();
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getAvailableCdnProviders(): array {
return ['jsdelivr', 'cdnjs', 'unpkg', 'custom'];
}
/**
* {@inheritdoc}
*/
public function getCdnProvidersForLibrary(string $library_id): array {
// Use cache if available.
if (isset($this->cdnProvidersCache[$library_id])) {
return $this->cdnProvidersCache[$library_id];
}
$plugin = $this->pluginManager->getPlugin($library_id);
if (!$plugin) {
return [];
}
$cdn_config = $plugin->getCdnConfig();
if (!$cdn_config) {
return [];
}
// Build options array with provider labels.
$providers = [];
foreach (array_keys($cdn_config) as $provider_key) {
// Create human-readable labels for providers.
$label = match($provider_key) {
'jsdelivr' => 'jsDelivr',
'cdnjs' => 'cdnjs',
'unpkg' => 'unpkg',
'custom' => $this->t('Custom CDN'),
default => ucfirst($provider_key),
};
$providers[$provider_key] = $label;
}
$this->cdnProvidersCache[$library_id] = $providers;
return $providers;
}
/**
* {@inheritdoc}
*/
public function getLibraryDefinition(): array {
$active = $this->getActiveLibrary();
$method = $this->getLoadingMethod();
// Check cache.
$cache_key = $active . '_' . $method;
if (isset($this->libraryDefinitionsCache[$cache_key])) {
return $this->libraryDefinitionsCache[$cache_key];
}
$plugin = $this->pluginManager->getPlugin($active);
if (!$plugin) {
return [];
}
// Validate library based on method.
if ($method === 'local' && !$plugin->isInstalled()) {
return [];
}
if ($method === 'cdn' && !$plugin->getCdnConfig()) {
return [];
}
$variant = $this->configAccessor->getBuildVariant();
// Build definition based on method.
if ($method === 'cdn') {
$definition = $this->buildCdnDefinition($plugin, $variant);
}
else {
$definition = $plugin->buildLibraryDefinition($variant);
}
// Add general settings.
$definition['drupalSettings']['countdown'] = [
'activeLibrary' => $active,
'libraryType' => $plugin->getType(),
'loadingMethod' => $method,
'initFunction' => $plugin->getInitFunction(),
];
// Allow other modules to alter the definition.
$this->moduleHandler->alter('countdown_library_definition', $definition, $active);
// Cache the definition.
$this->libraryDefinitionsCache[$cache_key] = $definition;
return $definition;
}
/**
* {@inheritdoc}
*/
public function buildCdnDefinition($plugin, bool $minified = TRUE): array {
$cdn_config = $plugin->getCdnConfig();
if (!$cdn_config) {
return [];
}
$provider = $this->getCdnProvider();
// Check if provider is available for this library.
if (!isset($cdn_config[$provider])) {
// Fall back to first available provider.
$provider = key($cdn_config);
}
$definition = [
'version' => $plugin->getRequiredVersion() ?: '1.0',
'dependencies' => $plugin->getDependencies(),
];
// Add CDN assets.
if (isset($cdn_config[$provider])) {
$cdn_assets = $cdn_config[$provider];
if (isset($cdn_assets['js'])) {
$js_url = $cdn_assets['js'];
// Use minified version if available and requested.
if ($minified && isset($cdn_assets['js_min'])) {
$js_url = $cdn_assets['js_min'];
}
$definition['js'][$js_url] = [
'type' => 'external',
'minified' => $minified,
'attributes' => [
'defer' => TRUE,
'crossorigin' => 'anonymous',
],
];
}
if (isset($cdn_assets['css'])) {
$css_url = $cdn_assets['css'];
// Use minified version if available and requested.
if ($minified && isset($cdn_assets['css_min'])) {
$css_url = $cdn_assets['css_min'];
}
$definition['css']['theme'][$css_url] = [
'type' => 'external',
'minified' => $minified,
];
}
}
return $definition;
}
/**
* {@inheritdoc}
*/
public function getAvailableLibraryOptions(string $for_method = NULL): array {
if ($for_method === NULL) {
$for_method = $this->getLoadingMethod();
}
if ($for_method === 'cdn') {
// Get CDN-capable libraries.
$options = [];
$cdn_plugins = $this->pluginManager->getCdnCapablePlugins();
foreach ($cdn_plugins as $plugin_id => $plugin) {
$label = $plugin->getLabel();
$version = $plugin->getRequiredVersion();
if ($version) {
$label .= ' (v' . $version . ')';
}
if ($plugin->isExperimental()) {
$label .= ' [' . $this->t('Experimental') . ']';
}
$options[$plugin_id] = $label;
}
return $options;
}
// Local method: get installed libraries.
return $this->pluginManager->getPluginOptions(TRUE, TRUE, FALSE);
}
/**
* {@inheritdoc}
*/
public function validateLibraryConfiguration(): array {
$messages = [];
$active = $this->getActiveLibrary();
$method = $this->getLoadingMethod();
// Validate active library.
$plugin = $this->pluginManager->getPlugin($active);
if (!$plugin) {
$messages[] = $this->t('Active library "@library" not found.', ['@library' => $active]);
return $messages;
}
// Validate based on method.
if ($method === 'local') {
if (!$plugin->isInstalled()) {
$messages[] = $this->t('Active library "@library" is not installed for local loading.', [
'@library' => $plugin->getLabel(),
]);
}
elseif (!$plugin->versionMeetsRequirements()) {
$messages[] = $this->t('Active library "@library" version does not meet requirements.', [
'@library' => $plugin->getLabel(),
]);
}
// Check if any libraries are available for local loading.
$installed = $this->pluginManager->getInstalledPlugins();
if (empty($installed)) {
$messages[] = $this->t('No countdown libraries are installed. Please install at least one library for local loading.');
}
}
elseif ($method === 'cdn') {
if (!$plugin->getCdnConfig()) {
$messages[] = $this->t('Active library "@library" does not support CDN loading.', [
'@library' => $plugin->getLabel(),
]);
}
// Check CDN provider.
$provider = $this->getCdnProvider();
$providers = $this->getCdnProvidersForLibrary($active);
if (!empty($providers) && !isset($providers[$provider])) {
$messages[] = $this->t('CDN provider "@provider" is not available for library "@library". Available providers: @providers', [
'@provider' => $provider,
'@library' => $plugin->getLabel(),
'@providers' => implode(', ', array_keys($providers)),
]);
}
// Check if any libraries support CDN.
$cdn_plugins = $this->pluginManager->getCdnCapablePlugins();
if (empty($cdn_plugins)) {
$messages[] = $this->t('No countdown libraries support CDN loading.');
}
}
return $messages;
}
/**
* {@inheritdoc}
*/
public function getLibraryRequirements(): array {
$requirements = [];
$method = $this->getLoadingMethod();
$active = $this->getActiveLibrary();
if ($method === 'cdn') {
// CDN mode requirements.
$cdn_plugins = $this->pluginManager->getCdnCapablePlugins();
if (empty($cdn_plugins)) {
$requirements['countdown_library'] = [
'title' => $this->t('Countdown Library (CDN)'),
'value' => $this->t('No CDN support'),
'severity' => REQUIREMENT_ERROR,
'description' => $this->t('No countdown libraries support CDN loading. Please switch to local loading or add CDN-capable libraries.'),
];
}
else {
$plugin = $this->pluginManager->getPlugin($active);
if ($plugin && $plugin->getCdnConfig()) {
$requirements['countdown_library'] = [
'title' => $this->t('Countdown Library (CDN)'),
'value' => $plugin->getLabel() . ' (CDN)',
'severity' => REQUIREMENT_OK,
'description' => $this->t('Library "@library" is configured for CDN loading. @count CDN-capable libraries available.', [
'@library' => $plugin->getLabel(),
'@count' => count($cdn_plugins),
]),
];
}
else {
$requirements['countdown_library'] = [
'title' => $this->t('Countdown Library (CDN)'),
'value' => $this->t('Configuration error'),
'severity' => REQUIREMENT_WARNING,
'description' => $this->t('Active library does not support CDN loading. Please select a CDN-capable library.'),
];
}
}
}
else {
// Local mode requirements.
$installed = $this->pluginManager->getInstalledPlugins();
if (empty($installed)) {
$requirements['countdown_library'] = [
'title' => $this->t('Countdown Library (Local)'),
'value' => $this->t('Not installed'),
'severity' => REQUIREMENT_ERROR,
'description' => $this->t('No countdown libraries found. Please install a countdown library in the libraries directory.'),
];
}
else {
$plugin = $this->pluginManager->getPlugin($active);
if ($plugin && $plugin->isInstalled()) {
$status = $plugin->getStatus();
$value = $plugin->getLabel();
// Add version info.
$version = $plugin->getInstalledVersion();
if ($version) {
$value .= ' (v' . $version . ')';
}
// Determine severity.
$severity = REQUIREMENT_OK;
if ($status['severity'] === 'error') {
$severity = REQUIREMENT_ERROR;
}
elseif ($status['severity'] === 'warning') {
$severity = REQUIREMENT_WARNING;
}
$description_parts = $status['messages'];
$description_parts[] = $this->t('@count libraries installed locally.', [
'@count' => count($installed),
]);
$requirements['countdown_library'] = [
'title' => $this->t('Countdown Library (Local)'),
'value' => $value,
'severity' => $severity,
'description' => implode(' ', $description_parts),
];
}
else {
$requirements['countdown_library'] = [
'title' => $this->t('Countdown Library (Local)'),
'value' => $this->t('Configuration error'),
'severity' => REQUIREMENT_WARNING,
'description' => $this->t('Active library is not installed. Please install it or select an installed library.'),
];
}
}
}
return $requirements;
}
/**
* {@inheritdoc}
*/
public function isAutoLoadEnabled(): bool {
return $this->configAccessor->isAutoLoadEnabled();
}
/**
* {@inheritdoc}
*/
public function setAutoLoad(bool $enabled): bool {
$this->configFactory->getEditable('countdown.settings')
->set('load', $enabled)
->save();
$this->clearCache();
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getBuildVariant(): bool {
return $this->configAccessor->getBuildVariant();
}
/**
* {@inheritdoc}
*/
public function setBuildVariant(bool $minified): bool {
$this->configFactory->getEditable('countdown.settings')
->set('build.variant', $minified)
->save();
$this->clearCache();
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getAssetTypes(): array {
// JS and CSS files are always loaded for main library.
// This method is kept for backward compatibility.
return [
'js' => TRUE,
'css' => TRUE,
];
}
/**
* {@inheritdoc}
*/
public function setAssetTypes(array $types): bool {
// This method is deprecated since JS/CSS are always loaded.
// Only extensions can be configured now.
// Kept for backward compatibility.
$this->clearCache();
return TRUE;
}
/**
* {@inheritdoc}
*/
public function switchLibrary(string $library_id, string $method = NULL): bool {
// Set method if provided.
if ($method !== NULL) {
$this->setLoadingMethod($method);
}
// Set the library.
return $this->setActiveLibrary($library_id);
}
/**
* {@inheritdoc}
*/
public function getLibraryInfo(string $library_id): ?array {
$plugin = $this->pluginManager->getPlugin($library_id);
if (!$plugin) {
return NULL;
}
return [
'id' => $library_id,
'label' => $plugin->getLabel(),
'description' => $plugin->getDescription(),
'type' => $plugin->getType(),
'installed' => $plugin->isInstalled(),
'version' => $plugin->getInstalledVersion(),
'required_version' => $plugin->getRequiredVersion(),
'version_ok' => $plugin->versionMeetsRequirements(),
'path' => $plugin->getLibraryPath(),
'cdn_support' => $plugin->getCdnConfig() !== NULL,
'cdn_providers' => $this->getCdnProvidersForLibrary($library_id),
'homepage' => $plugin->getHomepage(),
'author' => $plugin->getAuthor(),
'license' => $plugin->getLicense(),
'experimental' => $plugin->isExperimental(),
'status' => $plugin->getStatus(),
];
}
/**
* {@inheritdoc}
*/
public function getAllLibrariesInfo(): array {
$info = [];
$all_plugins = $this->pluginManager->getAllPlugins();
foreach ($all_plugins as $plugin_id => $plugin) {
$info[$plugin_id] = $this->getLibraryInfo($plugin_id);
}
return $info;
}
/**
* {@inheritdoc}
*/
public function clearCache(): void {
// Clear internal caches.
$this->libraryDefinitionsCache = [];
$this->cdnProvidersCache = [];
// Clear Drupal caches.
Cache::invalidateTags([
CountdownConstants::CACHE_TAG_SETTINGS,
CountdownConstants::CACHE_TAG_DISCOVERY,
'library_info',
]);
// Clear plugin manager cache.
$this->pluginManager->resetAllCaches();
// Clear discovery cache.
$this->libraryDiscovery->clearCache();
}
/**
* {@inheritdoc}
*/
public function getCdnProviders(): array {
$providers = [];
$cdn_plugins = $this->pluginManager->getCdnCapablePlugins();
foreach ($cdn_plugins as $plugin) {
$cdn_config = $plugin->getCdnConfig();
if ($cdn_config) {
foreach (array_keys($cdn_config) as $provider) {
$providers[$provider] = $provider;
}
}
}
// Add default providers.
$default_providers = $this->getAvailableCdnProviders();
foreach ($default_providers as $provider) {
$providers[$provider] = $provider;
}
return array_unique($providers);
}
/**
* {@inheritdoc}
*/
public function isLibraryCompatible(string $library_id, string $method): bool {
$plugin = $this->pluginManager->getPlugin($library_id);
if (!$plugin) {
return FALSE;
}
if ($method === 'local') {
return $plugin->isInstalled();
}
if ($method === 'cdn') {
return $plugin->getCdnConfig() !== NULL;
}
return FALSE;
}
}
