toolshed-8.x-1.x-dev/src/Strategy/StrategyManager.php

src/Strategy/StrategyManager.php
<?php

namespace Drupal\toolshed\Strategy;

use Drupal\Component\Discovery\DiscoverableInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\toolshed\Discovery\AttributeDiscovery;
use Drupal\toolshed\Discovery\YamlDiscovery;
use Drupal\toolshed\Event\StrategyDefinitionAlterEvent;
use Drupal\toolshed\Strategy\Attribute\Strategy;
use Drupal\toolshed\Strategy\Exception\StrategyNotFoundException;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
 * The default strategy manager definition class.
 *
 * This strategy manager primarily works with module defined strategies (or
 * always available strategies). If you want to have theme context definition
 * switching, then use the \Drupal\toolshed\Strategy\ThemeAwareStrategyManager.
 *
 * @see \Drupal\toolshed\Strategy\ThemeAwareStrategyManager
 */
abstract class StrategyManager implements StrategyManagerInterface {

  /**
   * The machine name to use for strategy discovery.
   *
   * @var string
   */
  protected string $discoveryName;

  /**
   * The attribute discovery subdirectory to search for strategy classes.
   *
   * The sub-directory path to search each module directory for strategy
   * classe. The path can start with the directory separator or not. This value
   * can be automatically generated from strategy attribute class.
   *
   * @var string|null
   */
  protected ?string $subdir = NULL;

  /**
   * The attribute class when using attribute discovery.
   *
   * @var class-string<\Drupal\toolshed\Strategy\Attribute\StrategyInterface>|null
   */
  protected ?string $attribute = NULL;

  /**
   * The interface that all strategy classes should implement.
   *
   * @var class-string<\Drupal\toolshed\Strategy\StrategyInterface>
   */
  protected string $strategyInterface = '\Drupal\toolshed\Strategy\StrategyInterface';

  /**
   * The strategy definitions available to this manager.
   *
   * @var \Drupal\toolshed\Strategy\StrategyDefinitionInterface[]|null
   */
  protected ?array $definitions;

  /**
   * Generated strategy instances already instantiated.
   *
   * @var \Drupal\toolshed\Strategy\StrategyInterface[]
   */
  protected array $instances = [];

  /**
   * The cache tags for caching the strategy definitions.
   *
   * @var string[]
   */
  protected array $cacheTags = [];

  /**
   * The strategy definition cache backend.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface|null
   */
  protected ?CacheBackendInterface $cacheBackend;

  /**
   * The alter definition event name to use when dispatching the alter event.
   *
   * Use the static::setAlterEvent() method to set this value and the event
   * dispatcher.
   *
   * @var string
   *
   * @see \Drupal\toolshed\Strategy\StrategyManager::setAlterEvent()
   */
  protected string $alterEventName = '';

  /**
   * The event dispatcher.
   *
   * Use the static::setAlterEvent() method to set this value and the event
   * dispatcher.
   *
   * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface|null
   *
   * @see \Drupal\toolshed\Strategy\StrategyManager::setAlterEvent()
   */
  protected ?EventDispatcherInterface $eventDispatcher;

  /**
   * Creates a new instance of the StrategyManager class.
   *
   * @param class-string<\Drupal\toolshed\Strategy\Attribute\StrategyInterface>|string|array $discover
   *   Either the strategy attribute class if using attribute discovery, the
   *   discovery name string if using YAML or a directory with the attribute,
   *   and subdirectory path to search for strategy implementations.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   The module handler.
   * @param \Drupal\Core\Cache\CacheBackendInterface|null $cacheBackend
   *   The cache backend to store strategy definitions.
   * @param string[] $cache_tags
   *   The cache tags to use when caching the strategy definitions.
   * @param \Drupal\toolshed\Strategy\StrategyFactoryInterface|null $factory
   *   The strategy factory service.
   */
  public function __construct(
    string|array $discover,
    protected ModuleHandlerInterface $moduleHandler,
    ?CacheBackendInterface $cacheBackend = NULL,
    array $cache_tags = [],
    protected ?StrategyFactoryInterface $factory = NULL,
  ) {
    if (is_array($discover)) {
      if (empty($discover['name'])) {
        throw new \InvalidArgumentException('A discovery "name" is require when using an array as the strategy discovery argument.');
      }

      $this->discoveryName = $discover['name'];
      $this->attribute = $discover['attribute'] ?? Strategy::class;
      $this->subdir = $discover['subdir'] ?? 'Strategy/' . str_replace(['.', '_'], ['/', ''], ucwords($this->discoveryName, '_.'));
    }
    elseif (preg_match('#\\\\?Drupal\\\\([^\\\\]+)(?:\\\\.+|)\\\\([^\\\\]+)$#i', $discover, $matches)) {
      $this->discoveryName = "{$matches[1]}.{$matches[2]}";
      $this->attribute = $discover;
      $this->subdir = 'Strategy/' . str_replace('_', '', ucwords($matches[1], '_')) . "/{$matches[2]}";
    }
    else {
      $this->discoveryName = $discover;
    }

    $this->setCacheBackend($cacheBackend, $cache_tags ?: [$this->discoveryName]);
  }

  /**
   * Sets the strategy definition alter event name and event dispatcher.
   *
   * Can be called from an constructor to initialize these services or can be
   * called from the services.yml file when defining services.
   *
   * The following is an example from the services definition:
   *
   * @code
   * strategy.manager.example:
   *   class: Drupal\toolshed\ExampleManager
   *   arguments:
   *     - 'name'
   *     - '@module_handler'
   *     - '@cache.discovery'
   *   calls:
   *     - [setAlterEvent, ['strategy_definition_alter', '@event_dispatcher']]
   * @endcode
   *
   * The latter is preferred, and keeps the constructor simpler. This also
   * makes it easier to apply this trait without having to the need to override
   * constructors.
   *
   * @param string $event_name
   *   The name of the strategy definition alter event.
   * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher service.
   */
  public function setAlterEvent(string $event_name, EventDispatcherInterface $event_dispatcher): void {
    $this->alterEventName = $event_name;
    $this->eventDispatcher = $event_dispatcher;
  }

  /**
   * {@inheritdoc}
   */
  public function getDiscoveryName(): string {
    return $this->discoveryName;
  }

  /**
   * {@inheritdoc}
   */
  public function clearCachedDefinitions(): void {
    $this->instances = [];
    unset($this->definitions);

    if ($this->cacheBackend) {
      $this->cacheBackend->delete($this->getCacheId());
    }
  }

  /**
   * {@inheritdoc}
   */
  public function hasDefinition(string $strategy_id, array $contexts = []): bool {
    return isset($this->getDefinitions($contexts)[$strategy_id]);
  }

  /**
   * {@inheritdoc}
   */
  public function getDefinition(string $strategy_id, array $contexts = []): StrategyDefinitionInterface {
    $definitions = $this->getDefinitions($contexts);

    if (isset($definitions[$strategy_id])) {
      return $definitions[$strategy_id];
    }

    throw new StrategyNotFoundException($strategy_id);
  }

  /**
   * {@inheritdoc}
   */
  public function getDefinitions(array $contexts = []): array {
    if (!isset($this->definitions)) {
      $cacheId = $this->getCacheId();

      if ($cached = $this->cacheGet($cacheId)) {
        $this->definitions = $cached->data;
      }
      else {
        $this->definitions = $this->findDefinitions();
        $this->cacheSet($cacheId, $this->definitions);
      }
    }

    return $this->definitions;
  }

  /**
   * {@inheritdoc}
   */
  public function getInstance(string $id, array $contexts = []): StrategyInterface {
    if (!isset($this->instances[$id])) {
      $definition = $this->getDefinition($id, $contexts);

      $instance = $this->getFactory()->create($id, $definition);
      $this->initInstance($id, $instance);
      $this->instances[$id] = $instance;
    }

    return $this->instances[$id];
  }

  /**
   * Get the cache identifier to use when storing strategy definitions.
   *
   * @return string
   *   The base cache identifier to use for storing defintions with.
   */
  protected function getCacheId(): string {
    $name = $this->getDiscoveryName();
    return "$name:definitions";
  }

  /**
   * Set the cache backend and cache tags.
   *
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
   *   The cache backend to use when storing the strategy definitions.
   * @param string[] $cache_tags
   *   The cache tags to use when caching the strategy definitions.
   */
  protected function setCacheBackend(CacheBackendInterface $cache_backend, array $cache_tags = []): void {
    $this->cacheTags = $cache_tags;
    $this->cacheBackend = $cache_backend;
  }

  /**
   * Get the matching cached value if one is available.
   *
   * @param string $cid
   *   The cache identifier to use to retrieve a cached value for.
   *
   * @return mixed|null
   *   The cached definition if a cached value is found matching the $cid.
   */
  protected function cacheGet(string $cid): mixed {
    return $this->cacheBackend ? $this->cacheBackend->get($cid) : NULL;
  }

  /**
   * Stores a value into the cache backend to the provided key.
   *
   * @param string $cid
   *   The cache identifier to use to store the data into the cache.
   * @param mixed $data
   *   The value to be cached.
   */
  protected function cacheSet(string $cid, $data): void {
    if ($this->cacheBackend) {
      $this->cacheBackend->set($cid, $data, Cache::PERMANENT, $this->cacheTags);
    }
  }

  /**
   * Gets the fully namespaced class of the fallback instance strategy class.
   *
   * @param mixed[] $definition
   *   The strategy definition to find a default class implementation for.
   *
   * @return class-string<\Drupal\toolshed\Strategy\StrategyInterface>
   *   The fully namespaced class names to create the strategy instance with.
   *
   * @throws \InvalidArgumentException
   *   When a default strategy is requested but not available.
   *
   * @see \Drupal\toolshed\Strategy\StrategyManager::processDefinition()
   */
  abstract protected function defaultStrategyClass(array $definition): string;

  /**
   * Converts the strategy definition into the strategy definition objects.
   *
   * @param string $id
   *   The strategy identifier.
   * @param mixed[] $definition
   *   The discovered and altered array definition of the strategy.
   *
   * @return \Drupal\toolshed\Strategy\StrategyDefinitionInterface
   *   The processed strategy definition.
   */
  protected function processDefinition(string $id, array $definition): StrategyDefinitionInterface {
    if (empty($definition['class'])) {
      $definition['class'] = $this->defaultStrategyClass($definition);
    }

    return new StrategyDefinition($id, $definition);
  }

  /**
   * Gets the discovery instance to use to find the strategy definitions.
   *
   * @return \Drupal\Component\Discovery\DiscoverableInterface
   *   The strategy definition discovery instance.
   */
  protected function getDiscovery(): DiscoverableInterface {
    $dirs = $this->moduleHandler->getModuleDirectories();

    if (!empty($this->attribute)) {
      $discovery = new AttributeDiscovery($this->moduleHandler, $this->subdir, $this->attribute);
    }
    else {
      $discovery = new YamlDiscovery($this->getDiscoveryName(), $dirs);
      $discovery->addTranslatableProperty('label', 'label_context');
      $discovery->addTranslatableProperty('description', 'description_context');
    }
    return $discovery;
  }

  /**
   * Sets the strategy factory instance for the manager.
   *
   * @param \Drupal\toolshed\Strategy\StrategyFactoryInterface $factory
   *   The factory to create strategy instances.
   *
   * @return self
   *   Returns itself for use with method chaining.
   */
  public function setFactory(StrategyFactoryInterface $factory): self {
    $this->factory = $factory;
    return $this;
  }

  /**
   * Get the strategy factory for building strategy instances from definitions.
   *
   * @return \Drupal\toolshed\Strategy\StrategyFactoryInterface
   *   The strategy factory to use when creating new strategy instances.
   */
  protected function getFactory(): StrategyFactoryInterface {
    if (!isset($this->factory)) {
      $this->factory = new ContainerStrategyFactory();
    }

    return $this->factory;
  }

  /**
   * Allows strategy managers to perform custom strategy initialization.
   *
   * @param string $id
   *   The strategy identifier.
   * @param \Drupal\toolshed\Strategy\StrategyInterface $strategy
   *   The strategy instance to initialize.
   */
  protected function initInstance(string $id, StrategyInterface $strategy): void {
  }

  /**
   * Find the strategy definitions using the discovery instance.
   *
   * @return \Drupal\toolshed\Strategy\StrategyDefinitionInterface[]
   *   An array of strategy definitions keyed by the strategy ID.
   */
  protected function findDefinitions(): array {
    $discovery = $this->getDiscovery();
    $byProvider = $discovery->findAll();

    // Ensure that all the definitions have their ID and provider info set.
    foreach ($byProvider as $provider => &$definitions) {
      foreach ($definitions as $id => &$definition) {
        $definition += [
          'id' => $id,
          'provider' => $provider,
          'provider_type' => 'module',
        ];
      }
    }
    unset($definitions, $definition);

    // Allow other modules to alter the discovered strategy definitions before
    // they are converted to the StrategyDefinitionInterface instances.
    if ($this->alterEventName && $this->eventDispatcher) {
      $event = new StrategyDefinitionAlterEvent('module', $byProvider);
      $this->eventDispatcher->dispatch($event, $this->alterEventName);
    }

    $strategies = [];
    // Flatten discovered strategies by ID. Theme based strategies which are
    // only active when the them is, should not be flattened.
    // @see \Drupal\toolshed\Strategy\ThemeAwareStrategyManager::findDefinitions()
    foreach ($byProvider as $provider => $definitions) {
      foreach ($definitions as $id => $def) {
        $strategies[$id] = $this->processDefinition($id, $def);
      }
    }

    return $strategies;
  }

}

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

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