countdown-8.x-1.8/src/Plugin/CountdownLibraryPluginBase.php
src/Plugin/CountdownLibraryPluginBase.php
<?php
declare(strict_types=1);
namespace Drupal\countdown\Plugin;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Datetime\TimeZoneFormHelper;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\countdown\Service\CountdownLibraryDiscoveryInterface;
use Drupal\countdown\Utility\CdnUrlBuilder;
use Drupal\countdown\Utility\ConfigAccessor;
use Drupal\countdown\Utility\LibraryPathResolver;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base class for Countdown Library plugins.
*
* @see \Drupal\countdown\Annotation\CountdownLibrary
* @see \Drupal\countdown\Plugin\CountdownLibraryPluginInterface
* @see \Drupal\countdown\CountdownLibraryPluginManager
* @see plugin_api
*/
abstract class CountdownLibraryPluginBase extends PluginBase implements CountdownLibraryPluginInterface, ContainerFactoryPluginInterface {
use StringTranslationTrait;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected FileSystemInterface $fileSystem;
/**
* The logger service.
*
* @var \Psr\Log\LoggerInterface
*/
protected LoggerInterface $logger;
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected CacheBackendInterface $cache;
/**
* The countdown library discovery service.
*
* @var \Drupal\countdown\Service\CountdownLibraryDiscoveryInterface
*/
protected CountdownLibraryDiscoveryInterface $libraryDiscovery;
/**
* The library path resolver.
*
* @var \Drupal\countdown\Utility\LibraryPathResolver
*/
protected LibraryPathResolver $pathResolver;
/**
* The CDN URL builder.
*
* @var \Drupal\countdown\Utility\CdnUrlBuilder
*/
protected CdnUrlBuilder $cdnBuilder;
/**
* The configuration accessor.
*
* @var \Drupal\countdown\Utility\ConfigAccessor
*/
protected ConfigAccessor $configAccessor;
/**
* Cached library path.
*
* @var string|null|false
*/
protected string|null|false $libraryPath = NULL;
/**
* Cached installed version.
*
* @var string|null|false
*/
protected string|null|false $installedVersion = NULL;
/**
* Constructs a CountdownLibraryPluginBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Psr\Log\LoggerInterface $logger
* The logger service.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\countdown\Service\CountdownLibraryDiscoveryInterface $library_discovery
* The countdown library discovery service.
* @param \Drupal\countdown\Utility\LibraryPathResolver $path_resolver
* The library path resolver.
* @param \Drupal\countdown\Utility\CdnUrlBuilder $cdn_builder
* The CDN URL builder.
* @param \Drupal\countdown\Utility\ConfigAccessor $config_accessor
* The configuration accessor.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
ModuleHandlerInterface $module_handler,
FileSystemInterface $file_system,
LoggerInterface $logger,
CacheBackendInterface $cache,
CountdownLibraryDiscoveryInterface $library_discovery,
LibraryPathResolver $path_resolver,
CdnUrlBuilder $cdn_builder,
ConfigAccessor $config_accessor,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moduleHandler = $module_handler;
$this->fileSystem = $file_system;
$this->logger = $logger;
$this->cache = $cache;
$this->libraryDiscovery = $library_discovery;
$this->pathResolver = $path_resolver;
$this->cdnBuilder = $cdn_builder;
$this->configAccessor = $config_accessor;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
// Create config accessor.
$config_accessor = new ConfigAccessor($container->get('config.factory'));
// Create path resolver with debug mode from config.
$path_resolver = new LibraryPathResolver(
$container->get('file_system'),
$container->get('logger.channel.countdown'),
$config_accessor->isDebugMode()
);
// Create CDN builder.
$cdn_builder = new CdnUrlBuilder();
// Return the fully constructed plugin instance.
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler'),
$container->get('file_system'),
$container->get('logger.channel.countdown'),
$container->get('cache.discovery'),
$container->get('countdown.library_discovery'),
$path_resolver,
$cdn_builder,
$config_accessor
);
}
/**
* {@inheritdoc}
*/
public function getLabel(): string {
return (string) $this->pluginDefinition['label'];
}
/**
* {@inheritdoc}
*/
public function getDescription(): string {
return (string) $this->pluginDefinition['description'];
}
/**
* {@inheritdoc}
*/
public function getType(): string {
return $this->pluginDefinition['type'] ?? 'external';
}
/**
* {@inheritdoc}
*/
public function isInstalled(): bool {
return $this->getLibraryPath() !== NULL;
}
/**
* {@inheritdoc}
*/
public function getLibraryPath(): ?string {
if ($this->libraryPath === NULL) {
$this->libraryPath = $this->findLibrary() ?: FALSE;
}
return $this->libraryPath !== FALSE ? $this->libraryPath : NULL;
}
/**
* {@inheritdoc}
*/
public function validateInstallation(string $path): bool {
return $this->pathResolver->validateInstallation(
$path,
$this->getRequiredFiles(),
$this->getAlternativePaths()
);
}
/**
* Finds the library installation path.
*
* @return string|null
* The library path or NULL if not found.
*/
protected function findLibrary(): ?string {
$path = $this->pathResolver->findLibrary(
$this->getPluginId(),
$this->getPossibleFolderNames()
);
if ($path && $this->validateInstallation(ltrim($path, '/'))) {
return $path;
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function detectVersion(string $path): ?string {
$path = ltrim($path, '/');
$base_path = DRUPAL_ROOT . '/' . $path;
// Try multiple detection strategies.
$strategies = [
'detectVersionFromPackageJson',
'detectVersionFromComposerJson',
'detectVersionFromBowerJson',
'detectVersionFromVersionFiles',
];
foreach ($strategies as $method) {
if (method_exists($this, $method)) {
$version = $this->$method($base_path);
if ($version) {
if ($this->configAccessor->isDebugMode()) {
$this->logger->debug('Detected version @version for library @library using @method', [
'@version' => $version,
'@library' => $this->getPluginId(),
'@method' => $method,
]);
}
return $version;
}
}
}
// Allow plugins to implement custom detection.
$version = $this->detectVersionCustom($base_path);
if ($version) {
return $version;
}
if ($this->configAccessor->isDebugMode()) {
$this->logger->debug('Could not detect version for library @library at path @path', [
'@library' => $this->getPluginId(),
'@path' => $path,
]);
}
return NULL;
}
/**
* Custom version detection for specific libraries.
*
* @param string $base_path
* The base path of the library.
*
* @return string|null
* The detected version or NULL.
*/
protected function detectVersionCustom(string $base_path): ?string {
// Override in child classes for custom detection.
return NULL;
}
/**
* {@inheritdoc}
*/
public function getRequiredVersion(): ?string {
return $this->pluginDefinition['version'] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function getInstalledVersion(): ?string {
if ($this->installedVersion === NULL) {
$path = $this->getLibraryPath();
if ($path) {
$this->installedVersion = $this->detectVersion(ltrim($path, '/')) ?: FALSE;
}
else {
$this->installedVersion = FALSE;
}
}
return $this->installedVersion !== FALSE ? $this->installedVersion : NULL;
}
/**
* {@inheritdoc}
*/
public function versionMeetsRequirements(): bool {
$installed = $this->getInstalledVersion();
$required = $this->getRequiredVersion();
if (!$installed) {
return FALSE;
}
if (!$required) {
return TRUE;
}
return version_compare($installed, $required, '>=');
}
/**
* {@inheritdoc}
*/
public function getAssets(bool $minified = TRUE): array {
// Delegate to asset map if defined.
$asset_map = $this->getAssetMap();
if (!empty($asset_map)) {
return $this->buildAssetsFromMap($asset_map, $minified);
}
// Legacy path-based asset discovery.
$path = $this->getLibraryPath();
if (!$path) {
return [];
}
$files = $this->pluginDefinition['files'] ?? [];
$variant = $minified ? 'production' : 'development';
$assets = [];
foreach (['js', 'css'] as $type) {
if (isset($files[$type][$variant])) {
$file_path = $files[$type][$variant];
$full_path = $path . '/' . $file_path;
if (file_exists(DRUPAL_ROOT . '/' . $full_path)) {
$assets[$type][] = [
'path' => $full_path,
'external' => FALSE,
'minified' => $minified,
'preprocess' => $minified,
];
}
}
}
return $assets;
}
/**
* Gets the asset map for this library.
*
* @return array
* Asset map with 'local' and 'cdn' keys.
*/
protected function getAssetMap(): array {
// Override in child classes to provide declarative asset mapping.
return [];
}
/**
* Builds assets from an asset map.
*
* @param array $asset_map
* The asset map.
* @param bool $minified
* Whether to use minified assets.
*
* @return array
* Array of assets keyed by type.
*/
protected function buildAssetsFromMap(array $asset_map, bool $minified): array {
$method = $this->configAccessor->getLoadingMethod();
$assets = [];
if ($method === 'cdn') {
// Build CDN assets.
$provider = $this->configAccessor->getCdnProvider();
if (isset($asset_map['cdn'][$provider])) {
$cdn_config = $asset_map['cdn'][$provider];
// Add JS.
if (isset($cdn_config['js'])) {
$assets['js'][] = [
'path' => $cdn_config['js'],
'external' => TRUE,
'minified' => TRUE,
'preprocess' => FALSE,
'attributes' => ['crossorigin' => 'anonymous'],
];
}
// Add CSS.
if (isset($cdn_config['css'])) {
$assets['css'][] = [
'path' => $cdn_config['css'],
'external' => TRUE,
'minified' => TRUE,
'preprocess' => FALSE,
];
}
}
}
else {
// Build local assets.
$path = $this->getLibraryPath();
if ($path && isset($asset_map['local'])) {
$variant = $minified ? 'production' : 'development';
foreach (['js', 'css'] as $type) {
if (isset($asset_map['local'][$type][$variant])) {
$file_path = $path . '/' . $asset_map['local'][$type][$variant];
if (file_exists(DRUPAL_ROOT . '/' . $file_path)) {
$assets[$type][] = [
'path' => $file_path,
'external' => FALSE,
'minified' => $minified,
'preprocess' => $minified,
];
}
}
}
}
}
return $assets;
}
/**
* {@inheritdoc}
*/
public function getRequiredFiles(): array {
return $this->pluginDefinition['required_files'] ?? [];
}
/**
* {@inheritdoc}
*/
public function getAlternativePaths(): array {
return $this->pluginDefinition['alternative_paths'] ?? [];
}
/**
* {@inheritdoc}
*/
public function getPossibleFolderNames(): array {
$names = $this->pluginDefinition['folder_names'] ?? [];
// Always include the plugin ID.
array_unshift($names, $this->getPluginId());
// Add NPM package name if different.
$npm_package = $this->getNpmPackage();
if ($npm_package && !in_array($npm_package, $names)) {
$names[] = $npm_package;
}
return array_unique($names);
}
/**
* {@inheritdoc}
*/
public function getInitFunction(): ?string {
return $this->pluginDefinition['init_function'] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function getNpmPackage(): ?string {
return $this->pluginDefinition['npm_package'] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function getCdnConfig(): ?array {
// Priority 1: Check annotation-based CDN configuration.
if (isset($this->pluginDefinition['cdn']) && is_array($this->pluginDefinition['cdn']) && !empty($this->pluginDefinition['cdn'])) {
return $this->pluginDefinition['cdn'];
}
// Priority 2: Fall back to asset map CDN configuration.
$asset_map = $this->getAssetMap();
if (isset($asset_map['cdn']) && is_array($asset_map['cdn']) && !empty($asset_map['cdn'])) {
return $asset_map['cdn'];
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getDependencies(): array {
return $this->pluginDefinition['dependencies'] ?? [];
}
/**
* {@inheritdoc}
*/
public function getHomepage(): ?string {
return $this->pluginDefinition['homepage'] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function getRepository(): ?string {
return $this->pluginDefinition['repository'] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function getAuthor(): ?string {
return $this->pluginDefinition['author'] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function getLicense(): ?string {
return $this->pluginDefinition['license'] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function getStatus(): array {
if (!$this->isInstalled()) {
return [
'installed' => FALSE,
'version_status' => 'not_installed',
'messages' => [$this->t('Library is not installed.')],
'severity' => 'error',
];
}
$status = [
'installed' => TRUE,
'version_status' => 'unknown',
'messages' => [],
'severity' => 'info',
];
$installed_version = $this->getInstalledVersion();
$required_version = $this->getRequiredVersion();
if ($installed_version) {
$status['messages'][] = $this->t('Installed version: @version', [
'@version' => $installed_version,
]);
if ($required_version) {
if ($this->versionMeetsRequirements()) {
$status['version_status'] = 'ok';
$status['messages'][] = $this->t('Version meets requirements (minimum: @min)', [
'@min' => $required_version,
]);
}
else {
$status['version_status'] = 'outdated';
$status['severity'] = 'warning';
$status['messages'][] = $this->t('Version is outdated. Minimum required: @min', [
'@min' => $required_version,
]);
}
}
else {
$status['version_status'] = 'ok';
$status['messages'][] = $this->t('No specific version requirements.');
}
}
else {
$status['messages'][] = $this->t('Version could not be detected.');
$status['severity'] = 'warning';
}
return $status;
}
/**
* {@inheritdoc}
*/
public function buildLibraryDefinition(bool $minified = TRUE): array {
$definition = [
'version' => $this->getInstalledVersion() ?: '1.0',
'dependencies' => $this->getDependencies(),
];
$assets = $this->getAssets($minified);
// Add JS assets.
if (!empty($assets['js'])) {
foreach ($assets['js'] as $js) {
$definition['js'][$js['path']] = [
'minified' => $js['minified'],
'preprocess' => $js['preprocess'],
'attributes' => ['defer' => TRUE],
];
}
}
// Add CSS assets.
if (!empty($assets['css'])) {
foreach ($assets['css'] as $css) {
$definition['css']['theme'][$css['path']] = [
'minified' => $css['minified'],
'preprocess' => $css['preprocess'],
];
}
}
// Add drupalSettings if needed.
$settings = $this->getLibrarySettings();
if (!empty($settings)) {
$definition['drupalSettings']['countdown'][$this->getPluginId()] = $settings;
}
return $definition;
}
/**
* {@inheritdoc}
*/
public function getLibrarySettings(): array {
return [
'library' => $this->getPluginId(),
'initFunction' => $this->getInitFunction(),
'version' => $this->getInstalledVersion(),
];
}
/**
* {@inheritdoc}
*/
public function getWeight(): int {
return $this->pluginDefinition['weight'] ?? 0;
}
/**
* {@inheritdoc}
*/
public function isExperimental(): bool {
return $this->pluginDefinition['experimental'] ?? FALSE;
}
/**
* {@inheritdoc}
*/
public function resetCache(): void {
$this->libraryPath = NULL;
$this->installedVersion = NULL;
$this->pathResolver->clearCache();
}
/**
* {@inheritdoc}
*/
public function hasExtensions(): bool {
return !empty($this->getAvailableExtensions());
}
/**
* {@inheritdoc}
*/
public function getAvailableExtensions(): array {
// Default implementation - override in child classes if needed.
return [];
}
/**
* {@inheritdoc}
*/
public function getExtensionGroups(): array {
// Default implementation - override in child classes if needed.
return [];
}
/**
* {@inheritdoc}
*/
public function buildAttachments(array $config): array {
$attachments = [];
// Extract context and library configuration.
$context = $config['context'] ?? 'global';
$library_config = $this->extractLibraryConfig($config, $context);
// Determine which extensions are needed based on configuration.
$required_extensions = $this->resolveRequiredExtensions($library_config, $config);
// Determine the library to attach based on type and method.
if ($this->getType() === 'core') {
// Core library uses timer definitions.
$library_name = $config['variant'] ? 'countdown/timer.min' : 'countdown/timer';
$attachments['#attached']['library'][] = $library_name;
// Also attach the core integration.
$integration_name = $config['variant'] ? 'countdown/integration.core.min' : 'countdown/integration.core';
$attachments['#attached']['library'][] = $integration_name;
}
else {
// External libraries: build library names for main and extensions.
$plugin_id = $this->getPluginId();
$base_library = $this->buildLibraryName($plugin_id, $config);
$attachments['#attached']['library'][] = 'countdown/' . $base_library;
// Attach required extensions.
foreach ($required_extensions as $extension_id) {
$extension_library = $this->buildExtensionLibraryName($extension_id, $config);
if ($extension_library) {
$attachments['#attached']['library'][] = 'countdown/' . $extension_library;
}
}
// Attach the specific integration file.
$integration_name = $config['variant']
? 'countdown/integration.' . $plugin_id . '.min'
: 'countdown/integration.' . $plugin_id;
$attachments['#attached']['library'][] = $integration_name;
}
// Create a web-accessible URL for the integration base path.
$base_url = \Drupal::request()->getBasePath();
// Get the module path relative to the Drupal root.
$module_path = $base_url . '/' . $this->moduleHandler->getModule('countdown')->getPath();
$integration_path = $module_path . '/js/integrations';
// Add drupalSettings for JavaScript initialization.
$attachments['#attached']['drupalSettings']['countdown'] = [
'activeLibrary' => $this->getPluginId(),
'libraryType' => $this->getType(),
'loadingMethod' => $config['method'],
'initFunction' => $this->getInitFunction(),
'settings' => $this->getLibrarySettings(),
'modulePath' => $module_path,
'integrationBasePath' => $integration_path,
'extensions' => $required_extensions,
'libraryConfig' => $library_config,
];
// Always attach the base integration library - BUT ONLY ONE VERSION!
$base_integration = $config['variant'] ? 'countdown/integration.min' : 'countdown/integration';
// Make sure we don't duplicate if already added.
if (!in_array($base_integration, $attachments['#attached']['library'])) {
$attachments['#attached']['library'][] = $base_integration;
}
// Add RTL support if enabled.
if (!empty($config['rtl'])) {
$attachments['#attached']['library'][] = 'core/drupal.rtl';
}
return $attachments;
}
/**
* Extract library configuration based on context.
*
* @param array $config
* The attachment configuration.
* @param string $context
* The context ('block', 'field', or 'global').
*
* @return array
* The extracted library configuration.
*/
protected function extractLibraryConfig(array $config, string $context): array {
switch ($context) {
case 'block':
// Block passes configuration directly.
return $config['block_config'][$this->getPluginId()] ?? [];
case 'field':
// Field passes configuration in field_config.
return $config['field_config'][$this->getPluginId()] ?? [];
case 'global':
// Global uses configuration from settings.
$global_config = $this->configAccessor->getConfig();
$library_specific = $global_config->get('library_specific') ?? [];
return is_array($library_specific) ? $library_specific : [];
default:
// Fallback to library_config if provided.
return $config['library_config'] ?? [];
}
}
/**
* Resolve required extensions based on library configuration.
*
* This method should be overridden in child classes that have extensions.
*
* @param array $library_config
* The library-specific configuration.
* @param array $config
* The full attachment configuration.
*
* @return array
* Array of required extension IDs.
*/
protected function resolveRequiredExtensions(array $library_config, array $config): array {
// Default implementation for libraries without extensions.
return [];
}
/**
* Build library name for loading.
*
* @param string $library_id
* The library ID.
* @param array $config
* The attachment configuration.
*
* @return string
* The library name for Drupal's library system.
*/
protected function buildLibraryName(string $library_id, array $config): string {
$library_name = $library_id;
if ($config['method'] === 'cdn') {
$library_name .= '_cdn';
}
if ($config['variant']) {
$library_name .= '.min';
}
return $library_name;
}
/**
* Build extension library name for loading.
*
* This method should be overridden in child classes that have extensions.
*
* @param string $extension_id
* The extension ID.
* @param array $config
* The attachment configuration.
*
* @return string|null
* The extension library name or NULL if not available.
*/
protected function buildExtensionLibraryName(string $extension_id, array $config): ?string {
// Default implementation - override in child classes.
return NULL;
}
/**
* {@inheritdoc}
*/
public function validateExtensions(array $extensions): array {
// Default implementation for plugins without extensions.
if (!$this->hasExtensions()) {
return [];
}
// Override in child classes that have extensions.
return [];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array &$form, FormStateInterface $form_state, array $default_values = []): void {
// Handle target_date which might be an array from form submission or a
// string from stored configuration.
$target_date_default = $this->getConfigValue($default_values, 'target_date', '');
// Normalize the target date value to ensure it's a valid string.
$target_date_default = $this->normalizeTargetDateValue($target_date_default);
// Only create DrupalDateTime if we have a valid date string.
$target_date_object = NULL;
if (!empty($target_date_default) && is_string($target_date_default)) {
try {
$target_date_object = DrupalDateTime::createFromFormat('Y-m-d\TH:i:s', $target_date_default);
}
catch (\Exception $e) {
// Invalid date format, leave as NULL.
$target_date_object = NULL;
}
}
// Target date and time configuration.
$form['target_date'] = [
'#type' => 'datetime',
'#title' => $this->t('Target Date and Time'),
'#description' => $this->t('The date and time to count down to or up from.'),
'#default_value' => $target_date_object,
'#required' => TRUE,
];
// Timezone configuration.
$form['timezone'] = [
'#type' => 'select',
'#title' => $this->t('Timezone'),
'#description' => $this->t('The timezone for the target date.'),
'#options' => TimeZoneFormHelper::getOptionsList(),
'#default_value' => $this->getConfigValue($default_values, 'timezone', date_default_timezone_get()),
'#required' => TRUE,
];
// Completion message.
$form['finish_message'] = [
'#type' => 'textfield',
'#title' => $this->t('Completion Message'),
'#description' => $this->t('Message to display when the countdown completes.'),
'#default_value' => $this->getConfigValue($default_values, 'finish_message', $this->t("Time's up!")),
'#maxlength' => 255,
];
// Create a fieldset for library-specific settings.
$form['library_specific'] = [
'#type' => 'details',
'#title' => $this->t('@library Settings', ['@library' => $this->getLabel()]),
'#description' => $this->t('Settings specific to the @library library.', ['@library' => $this->getLabel()]),
'#open' => TRUE,
];
}
/**
* Normalize target date value to a valid string format.
*
* This method handles various input formats and converts them to a
* consistent ISO-8601 string format or empty string if invalid.
*
* @param mixed $target_date_value
* The target date value in various possible formats.
*
* @return string
* The normalized date string in ISO-8601 format or empty string.
*/
protected function normalizeTargetDateValue($target_date_value): string {
// Handle NULL or empty values.
if (empty($target_date_value)) {
return '';
}
// Handle DrupalDateTime object.
if ($target_date_value instanceof DrupalDateTime) {
return $target_date_value->format('Y-m-d\TH:i:s');
}
// Handle regular DateTime object.
if ($target_date_value instanceof \DateTime) {
return $target_date_value->format('Y-m-d\TH:i:s');
}
// Handle array format (from form submission).
if (is_array($target_date_value)) {
// Check for object key containing DrupalDateTime.
if (isset($target_date_value['object']) && $target_date_value['object'] instanceof DrupalDateTime) {
return $target_date_value['object']->format('Y-m-d\TH:i:s');
}
// Check for date and time keys.
if (isset($target_date_value['date']) && isset($target_date_value['time'])) {
// Combine date and time into ISO format.
$combined = $target_date_value['date'] . 'T' . $target_date_value['time'];
// Validate the combined string is a valid date.
try {
$datetime = new \DateTime($combined);
return $datetime->format('Y-m-d\TH:i:s');
}
catch (\Exception $e) {
return '';
}
}
// Check for value key.
if (isset($target_date_value['value'])) {
return $this->normalizeTargetDateValue($target_date_value['value']);
}
// No recognizable array format.
return '';
}
// Handle string value.
if (is_string($target_date_value)) {
// Check for single 'T' which is invalid.
if ($target_date_value === 'T' || $target_date_value === 't') {
return '';
}
// Validate the string is a valid date.
try {
$datetime = new \DateTime($target_date_value);
return $datetime->format('Y-m-d\TH:i:s');
}
catch (\Exception $e) {
return '';
}
}
// Handle numeric timestamp.
if (is_numeric($target_date_value)) {
$timestamp = (int) $target_date_value;
// Validate reasonable timestamp range (year 1970 to 2100).
if ($timestamp > 0 && $timestamp < 4102444800) {
return gmdate('Y-m-d\TH:i:s', $timestamp);
}
}
// Unknown format or invalid value.
return '';
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state): void {
// Validate target date based on countdown timer.
$values = $form_state->getValues();
$target_date = $values['target_date'] ?? NULL;
if ($target_date instanceof DrupalDateTime) {
$now = new DrupalDateTime();
if ($target_date <= $now) {
$form_state->setErrorByName('target_date', $this->t('Target date must be in the future for countdown timers.'));
}
}
}
/**
* {@inheritdoc}
*/
public function getDefaultConfiguration(): array {
// Common default configuration for all libraries. Set a default target
// date 1 day in the future to avoid empty configurations.
$default_date = new \DateTime('+1 day');
return [
'target_date' => $default_date->format('Y-m-d\TH:i:s'),
'timezone' => date_default_timezone_get(),
'finish_message' => "Time's up!",
];
}
/**
* {@inheritdoc}
*/
public function getJavaScriptSettings(array $configuration): array {
// Common JavaScript settings. Handle target_date that might be in
// various formats from form submission.
$target_date = $this->getConfigValue($configuration, 'target_date', '');
// Normalize the target date value.
$target_date = $this->normalizeTargetDateValue($target_date);
// If still no valid date, use a default future date.
if (empty($target_date)) {
$default_date = new \DateTime('+1 day');
$target_date = $default_date->format('Y-m-d\TH:i:s');
}
$settings = [
'target_date' => $target_date,
'timezone' => $this->getConfigValue($configuration, 'timezone', date_default_timezone_get()),
'finish_message' => $this->getConfigValue($configuration, 'finish_message', "Time's up!"),
];
return $settings;
}
/**
* Helper method to get a configuration value with a default.
*
* @param array $values
* The configuration values array.
* @param string $key
* The configuration key.
* @param mixed $default
* The default value if key doesn't exist.
*
* @return mixed
* The configuration value or default.
*/
protected function getConfigValue(array $values, string $key, $default = NULL) {
return $values[$key] ?? $default;
}
/**
* Detects version from package.json file.
*
* @param string $base_path
* The base path of the library.
*
* @return string|null
* The detected version or NULL.
*/
protected function detectVersionFromPackageJson(string $base_path): ?string {
$locations = ['/package.json', '/dist/package.json', '/src/package.json'];
foreach ($locations as $location) {
$file = $base_path . $location;
if (file_exists($file)) {
try {
$content = file_get_contents($file);
$data = json_decode($content, TRUE);
if (json_last_error() === JSON_ERROR_NONE && !empty($data['version'])) {
return $this->normalizeVersion($data['version']);
}
}
catch (\Exception $e) {
// Continue to next location.
}
}
}
return NULL;
}
/**
* Detects version from composer.json file.
*
* @param string $base_path
* The base path of the library.
*
* @return string|null
* The detected version or NULL.
*/
protected function detectVersionFromComposerJson(string $base_path): ?string {
$file = $base_path . '/composer.json';
if (file_exists($file)) {
try {
$content = file_get_contents($file);
$data = json_decode($content, TRUE);
if (json_last_error() === JSON_ERROR_NONE && !empty($data['version'])) {
return $this->normalizeVersion($data['version']);
}
}
catch (\Exception $e) {
// Continue.
}
}
return NULL;
}
/**
* Detects version from bower.json file.
*
* @param string $base_path
* The base path of the library.
*
* @return string|null
* The detected version or NULL.
*/
protected function detectVersionFromBowerJson(string $base_path): ?string {
$file = $base_path . '/bower.json';
if (file_exists($file)) {
try {
$content = file_get_contents($file);
$data = json_decode($content, TRUE);
if (json_last_error() === JSON_ERROR_NONE && !empty($data['version'])) {
return $this->normalizeVersion($data['version']);
}
}
catch (\Exception $e) {
// Continue.
}
}
return NULL;
}
/**
* Detects version from version files.
*
* @param string $base_path
* The base path of the library.
*
* @return string|null
* The detected version or NULL.
*/
protected function detectVersionFromVersionFiles(string $base_path): ?string {
$files = ['VERSION', 'VERSION.txt', 'version.txt', '.version', 'version'];
foreach ($files as $file) {
$version_file = $base_path . '/' . $file;
if (file_exists($version_file)) {
$content = trim(file_get_contents($version_file));
if (preg_match('/^v?(\d+\.\d+(?:\.\d+)?(?:[-+].+)?)$/i', $content, $matches)) {
return $this->normalizeVersion($matches[1]);
}
}
}
return NULL;
}
/**
* Normalizes version string.
*
* @param string $version
* The raw version string.
*
* @return string
* The normalized version string.
*/
protected function normalizeVersion(string $version): string {
$version = preg_replace('/^v/i', '', trim($version));
$version = trim($version, '"\'');
return $version;
}
}
