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;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc