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