diff --git a/core/lib/Drupal/Component/Plugin/Derivative/DerivativeInterface.php b/core/lib/Drupal/Component/Plugin/Derivative/DerivativeInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..bb0706907582c1288c6ae06258600f92dc0f0f23 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Derivative/DerivativeInterface.php @@ -0,0 +1,44 @@ +<?php + +/** + * @file + * Definition of Drupal\Component\Plugin\Derivative\DerivativeInterface. + */ + +namespace Drupal\Component\Plugin\Derivative; + +/** + * Plugin interface for derivative plugin handling. + */ +interface DerivativeInterface { + + /** + * Returns the definition of a derivative plugin. + * + * @param string $derivative_id + * The derivative id. The id must uniquely identify the derivative within a + * given base plugin, but derivative ids can be reused across base plugins. + * @param array $base_plugin_definition + * The definition array of the base plugin from which the derivative plugin + * is derived. + * + * @return array + * The full definition array of the derivative plugin, typically a merge of + * $base_plugin_definition with extra derivative-specific information. NULL + * if the derivative doesn't exist. + */ + public function getDerivativeDefinition($derivative_id, array $base_plugin_definition); + + /** + * Returns the definition of all derivatives of a base plugin. + * + * @param array $base_plugin_definition + * The definition array of the base plugin. + * @return array + * An array of full derivative definitions keyed on derivative id. + * + * @see getDerivativeDefinition() + */ + public function getDerivativeDefinitions(array $base_plugin_definition); + +} diff --git a/core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscoveryDecorator.php b/core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscoveryDecorator.php new file mode 100644 index 0000000000000000000000000000000000000000..e245874d4c5cc03fc9c1ef9ab77da417baf80fcd --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscoveryDecorator.php @@ -0,0 +1,157 @@ +<?php + +/** + * @file + * Definition of Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator. + */ + +namespace Drupal\Component\Plugin\Discovery; + +/** + * Base class providing the tools for a plugin discovery to be derivative aware. + * + * Provides a decorator that allows the use of plugin derivatives for normal + * implementations DiscoveryInterface. + */ +class DerivativeDiscoveryDecorator implements DiscoveryInterface { + + protected $derivativeFetchers = array(); + protected $decorated; + + /** + * Creates a Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator + * object. + * + * @param DiscoveryInterface $discovery + * The parent object implementing DiscoveryInterface that is being + * decorated. + */ + public function __construct(DiscoveryInterface $decorated) { + $this->decorated = $decorated; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinition(). + */ + public function getDefinition($plugin_id) { + + list($base_plugin_id, $derivative_id) = $this->decodePluginId($plugin_id); + + $plugin_definition = $this->decorated->getDefinition($base_plugin_id); + if (isset($plugin_definition)) { + $derivative_fetcher = $this->getDerivativeFetcher($base_plugin_id, $plugin_definition); + if ($derivative_fetcher) { + $plugin_definition = $derivative_fetcher->getDerivativeDefinition($derivative_id, $plugin_definition); + } + } + + return $plugin_definition; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinitions(). + */ + public function getDefinitions() { + $plugin_definitions = $this->decorated->getDefinitions(); + return $this->getDerivatives($plugin_definitions); + } + + /** + * Adds derivatives to a list of plugin definitions. + * + * This should be called by the class extending this in + * DiscoveryInterface::getDefinitions(). + */ + protected function getDerivatives(array $base_plugin_definitions) { + $plugin_definitions = array(); + foreach ($base_plugin_definitions as $base_plugin_id => $plugin_definition) { + $derivative_fetcher = $this->getDerivativeFetcher($base_plugin_id, $plugin_definition); + if ($derivative_fetcher) { + $derivative_definitions = $derivative_fetcher->getDerivativeDefinitions($plugin_definition); + foreach ($derivative_definitions as $derivative_id => $derivative_definition) { + $plugin_id = $this->encodePluginId($base_plugin_id, $derivative_id); + $plugin_definitions[$plugin_id] = $derivative_definition; + } + } + else { + $plugin_definitions[$base_plugin_id] = $plugin_definition; + } + } + + return $plugin_definitions; + } + + /** + * Decodes derivative id and plugin id from a string. + * + * @param string $plugin_id + * Plugin identifier that may point to a derivative plugin. + * + * @return array + * An array with the base plugin id as the first index and the derivative id + * as the second. If there is no derivative id it will be null. + */ + protected function decodePluginId($plugin_id) { + // Try and split the passed plugin definition into a plugin and a + // derivative id. We don't need to check for !== FALSE because a leading + // colon would break the derivative system and doesn't makes sense. + if (strpos($plugin_id, ':')) { + return explode(':', $plugin_id, 2); + } + + return array($plugin_id, NULL); + } + + /** + * Encodes plugin and derivative id's into a string. + * + * @param string $base_plugin_id + * The base plugin identifier. + * @param string $derivative_id + * The derivative identifier. + * + * @return string + * A uniquely encoded combination of the $base_plugin_id and $derivative_id. + */ + protected function encodePluginId($base_plugin_id, $derivative_id) { + if ($derivative_id) { + return "$base_plugin_id:$derivative_id"; + } + + // By returning the unmerged plugin_id, we are able to support derivative + // plugins that support fetching the base definitions. + return $base_plugin_id; + } + + /** + * Finds a Drupal\Component\Plugin\Discovery\DerivativeInterface. + * + * This Drupal\Component\Plugin\Discovery\DerivativeInterface can fetch + * derivatives for the plugin. + * + * @param string $base_plugin_id + * The base plugin id of the plugin. + * @param array $base_definition + * The base plugin definition to build derivatives. + * + * @return Drupal\Component\Plugin\Discovery\DerivativeInterface|null + * A DerivativeInterface or null if none exists for the plugin. + */ + protected function getDerivativeFetcher($base_plugin_id, array $base_definition) { + if (!isset($this->derivativeFetchers[$base_plugin_id])) { + $this->derivativeFetchers[$base_plugin_id] = FALSE; + if (isset($base_definition['derivative'])) { + $class = $base_definition['derivative']; + $this->derivativeFetchers[$base_plugin_id] = new $class($base_plugin_id); + } + } + return $this->derivativeFetchers[$base_plugin_id] ?: NULL; + } + + /** + * Passes through all unknown calls onto the decorated object. + */ + public function __call($method, $args) { + return call_user_func_array(array($this->decorated, $method), $args); + } +} diff --git a/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryInterface.php b/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..dee79eae09ec8d3b63d0d3702f230c641f7ae45e --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryInterface.php @@ -0,0 +1,35 @@ +<?php + +/** + * @file + * Definition of Drupal\Component\Plugin\Discovery\DiscoveryInterface. + */ + +namespace Drupal\Component\Plugin\Discovery; + +/** + * An interface defining the minimum requirements of building a plugin + * discovery component. + */ +interface DiscoveryInterface { + + /** + * Gets a specific plugin definition. + * + * @param string $plugin_id + * A plugin id. + * + * @return array + * A plugin definition. + */ + public function getDefinition($plugin_id); + + /** + * Gets the definition of all plugins for this type. + * + * @return array + * An array of plugin definitions. + */ + public function getDefinitions(); + +} diff --git a/core/lib/Drupal/Component/Plugin/Discovery/StaticDiscovery.php b/core/lib/Drupal/Component/Plugin/Discovery/StaticDiscovery.php new file mode 100644 index 0000000000000000000000000000000000000000..de74fb5a902e3215013ddec00536bea224075f9b --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Discovery/StaticDiscovery.php @@ -0,0 +1,50 @@ +<?php + +/** + * @file + * Definition of Drupal\Component\Plugin\Discovery\StaticDiscovery. + */ + +namespace Drupal\Component\Plugin\Discovery; + +/** + * A discovery mechanism that allows plugin definitions to be manually + * registered rather than actively discovered. + */ +class StaticDiscovery implements DiscoveryInterface { + + /** + * The array of plugin definitions, keyed by plugin id. + * + * @var array + */ + protected $definitions = array(); + + /** + * Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinition(). + */ + public function getDefinition($base_plugin_id) { + return isset($this->definitions[$base_plugin_id]) ? $this->definitions[$base_plugin_id] : NULL; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinitions(). + */ + public function getDefinitions() { + return $this->definitions; + } + + /** + * Sets a plugin definition. + */ + public function setDefinition($plugin, array $definition) { + $this->definitions[$plugin] = $definition; + } + + /** + * Deletes a plugin definition. + */ + public function deleteDefinition($plugin) { + unset($this->definitions[$plugin]); + } +} diff --git a/core/lib/Drupal/Component/Plugin/Exception/ExceptionInterface.php b/core/lib/Drupal/Component/Plugin/Exception/ExceptionInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..57fb25bfc528702ab6eb18dff414bfbfde19b721 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Exception/ExceptionInterface.php @@ -0,0 +1,12 @@ +<?php +/** + * @file + * Definition of Drupal\Component\Plugin\Exception\ExceptionInterface. + */ + +namespace Drupal\Component\Plugin\Exception; + +/** + * Exception interface for all exceptions thrown by the Plugin component. + */ +interface ExceptionInterface { } diff --git a/core/lib/Drupal/Component/Plugin/Exception/InvalidDecoratedMethod.php b/core/lib/Drupal/Component/Plugin/Exception/InvalidDecoratedMethod.php new file mode 100644 index 0000000000000000000000000000000000000000..a3ef8e636c95f6935435817198d8be1cd7fb5450 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Exception/InvalidDecoratedMethod.php @@ -0,0 +1,17 @@ +<?php +/** +* @file +* Definition of Drupal\Core\Plugin\Exception\InvalidDecoratedMethod. +*/ + +namespace Drupal\Component\Plugin\Exception; + +use Drupal\Component\Plugin\Exception\ExceptionInterface; +use \BadMethodCallException; + +/** + * Exception thrown when a decorator's _call() method is triggered, but the + * decorated object does not contain the requested method. + * + */ +class InvalidDecoratedMethod extends BadMethodCallException implements ExceptionInterface { } diff --git a/core/lib/Drupal/Component/Plugin/Exception/MapperExceptionInterface.php b/core/lib/Drupal/Component/Plugin/Exception/MapperExceptionInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..222b5473d0aaee0ea9cd1780ea66bbcb86853ee9 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Exception/MapperExceptionInterface.php @@ -0,0 +1,13 @@ +<?php +/** + * @file + * Base exception interface for grouping mapper exceptions. + */ + +namespace Drupal\Component\Plugin\Exception; + +/** + * Extended interface for exceptions thrown specifically by the Mapper subsystem + * within the Plugin component. + */ +interface MapperExceptionInterface extends ExceptionInterface { } diff --git a/core/lib/Drupal/Component/Plugin/Exception/PluginException.php b/core/lib/Drupal/Component/Plugin/Exception/PluginException.php new file mode 100644 index 0000000000000000000000000000000000000000..f83bdad3d29ca5cccedbeac61d1723a44bc9776f --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Exception/PluginException.php @@ -0,0 +1,15 @@ +<?php +/** + * @file + * Definition of Drupal\Component\Plugin\Exception\PluginException. + */ + +namespace Drupal\Component\Plugin\Exception; + +use Exception; + +/** + * Generic Plugin exception class to be thrown when no more specific class + * is applicable. + */ +class PluginException extends Exception implements ExceptionInterface { } diff --git a/core/lib/Drupal/Component/Plugin/Factory/DefaultFactory.php b/core/lib/Drupal/Component/Plugin/Factory/DefaultFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..39a260b6b8c97cc5bbe2d2882df96125706234c3 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Factory/DefaultFactory.php @@ -0,0 +1,71 @@ +<?php +/** + * @file + * Definition of Drupal\Component\Plugin\Factory\DefaultFactory. + */ + +namespace Drupal\Component\Plugin\Factory; + +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\Component\Plugin\Exception\PluginException; +use Drupal\Component\Plugin\Derivative\DerivativeInterface; + +/** + * Default plugin factory. + * + * Instantiates plugin instances by passing the full configuration array as a + * single constructor argument. Plugin types wanting to support plugin classes + * with more flexible constructor signatures can do so by using an alternate + * factory such as Drupal\Component\Plugin\Factory\ReflectionFactory. + */ +class DefaultFactory implements FactoryInterface { + + /** + * The object that retrieves the definitions of the plugins that this factory instantiates. + * + * The plugin definition includes the plugin class and possibly other + * information necessary for proper instantiation. + * + * @var Drupal\Component\Plugin\Discovery\DiscoveryInterface + */ + protected $discovery; + + /** + * Constructs a Drupal\Component\Plugin\Factory\DefaultFactory object. + */ + public function __construct(DiscoveryInterface $discovery) { + $this->discovery = $discovery; + } + + /** + * Implements Drupal\Component\Plugin\Factory\FactoryInterface::createInstance(). + */ + public function createInstance($plugin_id, array $configuration) { + $plugin_class = $this->getPluginClass($plugin_id); + return new $plugin_class($configuration, $plugin_id, $this->discovery); + } + + /** + * Finds the class relevant for a given plugin. + * + * @param array $plugin_id + * The id of a plugin. + * + * @return string + * The appropriate class name. + */ + protected function getPluginClass($plugin_id) { + $plugin_definition = $this->discovery->getDefinition($plugin_id); + if (empty($plugin_definition['class'])) { + throw new PluginException('The plugin did not specify an instance class.'); + } + + $class = $plugin_definition['class']; + + if (!class_exists($class)) { + throw new PluginException(sprintf('Plugin instance class "%s" does not exist.', $class)); + } + + return $class; + } +} diff --git a/core/lib/Drupal/Component/Plugin/Factory/FactoryInterface.php b/core/lib/Drupal/Component/Plugin/Factory/FactoryInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..7954745a1cb3230799ee703b2fe1e80b88f5f824 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Factory/FactoryInterface.php @@ -0,0 +1,29 @@ +<?php +/** + * @file + * Definition of Drupal\Component\Plugin\Factory\FactoryInterface. + */ + +namespace Drupal\Component\Plugin\Factory; + +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; + +/** + * Factory interface implemented by all plugin factories. + */ +interface FactoryInterface { + + /** + * Returns a preconfigured instance of a plugin. + * + * @param string $plugin_id + * The id of the plugin being instantiated. + * @param array $configuration + * An array of configuration relevant to the plugin instance. + * + * @return object + * A fully configured plugin instance. + */ + public function createInstance($plugin_id, array $configuration); + +} diff --git a/core/lib/Drupal/Component/Plugin/Factory/ReflectionFactory.php b/core/lib/Drupal/Component/Plugin/Factory/ReflectionFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..a81b868d6fa975e0ae982e6eb8b269ebda6d53e0 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Factory/ReflectionFactory.php @@ -0,0 +1,83 @@ +<?php +/** + * @file + * Definition of Drupal\Component\Plugin\Factory\ReflectionFactory. + */ + +namespace Drupal\Component\Plugin\Factory; + +use ReflectionClass; + +/** + * A plugin factory that maps instance configuration to constructor arguments. + * + * Provides logic for any basic plugin type that needs to provide individual + * plugins based upon some basic logic. + */ +class ReflectionFactory extends DefaultFactory { + + /** + * Implements Drupal\Component\Plugin\Factory\FactoryInterface::createInstance(). + */ + public function createInstance($plugin_id, array $configuration) { + $plugin_class = $this->getPluginClass($plugin_id); + + // Lets figure out of there's a constructor for this class and pull + // arguments from the $options array if so to populate it. + $reflector = new ReflectionClass($plugin_class); + if ($reflector->hasMethod('__construct')) { + $arguments = $this->getInstanceArguments($reflector, $plugin_id, $configuration); + $instance = $reflector->newInstanceArgs($arguments); + } + else { + $instance = new $plugin_class(); + } + + return $instance; + } + + /** + * Inspects the plugin class and build a list of arguments for the constructor. + * + * This is provided as a helper method so factories extending this class can + * replace this and insert their own reflection logic. + * + * @param ReflectionClass $reflector + * The reflector object being used to inspect the plugin class. + * @param string $plugin_id + * The identifier of the plugin implementation. + * @param array $configuration + * An array of configuration that may be passed to the instance. + * + * @return array + * An array of arguments to be passed to the constructor. + */ + protected function getInstanceArguments(ReflectionClass $reflector, $plugin_id, array $configuration) { + + $arguments = array(); + foreach ($reflector->getMethod('__construct')->getParameters() as $param) { + $param_name = $param->getName(); + $param_class = $param->getClass(); + + if ($param_name == 'plugin_id') { + $arguments[] = $plugin_id; + } + elseif ($param_name == 'configuration') { + $arguments[] = $configuration; + } + elseif ($param_class && $param_class->isInstance($this->discovery)) { + $arguments[] = $this->discovery; + } + elseif (isset($configuration[$param_name]) || array_key_exists($param_name, $configuration)) { + $arguments[] = $configuration[$param_name]; + } + elseif ($param->isDefaultValueAvailable()) { + $arguments[] = $param->getDefaultValue(); + } + else { + $arguments[] = NULL; + } + } + return $arguments; + } +} diff --git a/core/lib/Drupal/Component/Plugin/Mapper/MapperInterface.php b/core/lib/Drupal/Component/Plugin/Mapper/MapperInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..03610e6490cc1e50cca035440d367f7ce467620e --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Mapper/MapperInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * @file + * Definition of Drupal\Component\Plugin\Mapper\MapperInterface. + */ + +namespace Drupal\Component\Plugin\Mapper; + +/** + * Plugin mapper interface. + * + * Plugin mappers are responsible for mapping a plugin request to its + * implementation. For example, it might map a cache bin to a memcache bin. + * + * Mapper objects incorporate the best practices of retrieving configurations, + * type information, and factory instantiation. + */ +interface MapperInterface { + + /** + * Returns a preconfigured instance of a plugin. + * + * @param array $options + * An array of options that can be used to determine a suitable plugin to + * instantiate and how to configure it. + * + * @return object + * A fully configured plugin instance. The interface of the plugin instance + * will depends on the plugin type. + */ + public function getInstance(array $options); + +} diff --git a/core/lib/Drupal/Component/Plugin/PluginBase.php b/core/lib/Drupal/Component/Plugin/PluginBase.php new file mode 100644 index 0000000000000000000000000000000000000000..58c5a3bd2f07d030df669f8098fa8afa941b06c7 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/PluginBase.php @@ -0,0 +1,70 @@ +<?php +/** + * @file + * Definition of Drupal\Component\Plugin\PluginBase + */ + +namespace Drupal\Component\Plugin; + +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; + +/** + * Base class for plugins wishing to support metadata inspection. + */ +abstract class PluginBase implements PluginInspectionInterface { + + /** + * The discovery object. + * + * @var Drupal\Component\Plugin\Discovery\DiscoveryInterface + */ + protected $discovery; + + /** + * The plugin_id. + * + * @var string + */ + protected $plugin_id; + + /** + * Configuration information passed into the plugin. + * + * @var array + */ + protected $configuration; + + /** + * Constructs a Drupal\Component\Plugin\PluginBase 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 DiscoveryInterface $discovery + * The Discovery class that holds access to the plugin implementation + * definition. + */ + public function __construct(array $configuration, $plugin_id, DiscoveryInterface $discovery) { + $this->configuration = $configuration; + $this->plugin_id = $plugin_id; + $this->discovery = $discovery; + } + + /** + * Implements Drupal\Component\Plugin\PluginInterface::getPluginId(). + */ + public function getPluginId() { + return $this->plugin_id; + } + + /** + * Implements Drupal\Component\Plugin\PluginInterface::getDefinition(). + */ + public function getDefinition() { + return $this->discovery->getDefinition($this->plugin_id); + } + + // Note: Plugin configuration is optional so its left to the plugin type to + // require a getter as part of its interface. +} diff --git a/core/lib/Drupal/Component/Plugin/PluginInspectionInterface.php b/core/lib/Drupal/Component/Plugin/PluginInspectionInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..9157d7c43179a722ff10514befc0044323104057 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/PluginInspectionInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * @file + * Definition of Drupal\Component\Plugin\PluginInspectionInterface. + */ + +namespace Drupal\Component\Plugin; + +/** + * Plugin interface for providing some metadata inspection. + * + * This interface provides some simple tools for code recieving a plugin to + * interact with the plugin system. + */ +interface PluginInspectionInterface { + + /** + * Returns the plugin_id of the plugin instance. + * + * @return string + * The plugin_id of the plugin instance. + */ + public function getPluginId(); + + /** + * Returns the definition of the plugin implementation. + * + * @return array + * The plugin definition, as returned by the discovery object used by the + * plugin manager. + */ + public function getDefinition(); +} diff --git a/core/lib/Drupal/Component/Plugin/PluginManagerBase.php b/core/lib/Drupal/Component/Plugin/PluginManagerBase.php new file mode 100644 index 0000000000000000000000000000000000000000..73f1b5542dac9f5c9784ffe5bd7163c7c0bf25af --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/PluginManagerBase.php @@ -0,0 +1,92 @@ +<?php + +/** + * @file + * Definition of Drupal\Component\Plugin\PluginManagerBase + */ + +namespace Drupal\Component\Plugin; + +/** + * Base class for plugin managers. + */ +abstract class PluginManagerBase implements PluginManagerInterface { + + /** + * The object that discovers plugins managed by this manager. + * + * @var Drupal\Component\Plugin\Discovery\DiscoveryInterface + */ + protected $discovery; + + /** + * The object that instantiates plugins managed by this manager. + * + * @var Drupal\Component\Plugin\Factory\FactoryInterface + */ + protected $factory; + + /** + * The object that returns the preconfigured plugin instance appropriate for a particular runtime condition. + * + * @var Drupal\Component\Plugin\Mapper\MapperInterface + */ + protected $mapper; + + /** + * A set of defaults to be referenced by $this->processDefinition() if + * additional processing of plugins is necessary or helpful for development + * purposes. + * + * @var array + */ + protected $defaults = array(); + + /** + * Implements Drupal\Component\Plugin\PluginManagerInterface::getDefinition(). + */ + public function getDefinition($plugin_id) { + $definition = $this->discovery->getDefinition($plugin_id); + if (isset($definition)) { + $this->processDefinition($definition, $plugin_id); + } + return $definition; + } + + /** + * Implements Drupal\Component\Plugin\PluginManagerInterface::getDefinitions(). + */ + public function getDefinitions() { + $definitions = $this->discovery->getDefinitions(); + foreach ($definitions as $plugin_id => &$definition) { + $this->processDefinition($definition, $plugin_id); + } + + return $definitions; + } + + /** + * Implements Drupal\Component\Plugin\PluginManagerInterface::createInstance(). + */ + public function createInstance($plugin_id, array $configuration = array()) { + return $this->factory->createInstance($plugin_id, $configuration); + } + + /** + * Implements Drupal\Component\Plugin\PluginManagerInterface::getInstance(). + */ + public function getInstance(array $options) { + return $this->mapper->getInstance($options); + } + + /** + * Performs extra processing on plugin definitions. + * + * By default we add defaults for the type to the definition. If a type has + * additional processing logic they can do that by replacing or extending the + * method. + */ + protected function processDefinition(&$definition, $plugin_id) { + $definition += $this->defaults; + } +} diff --git a/core/lib/Drupal/Component/Plugin/PluginManagerInterface.php b/core/lib/Drupal/Component/Plugin/PluginManagerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..a7d74a8092e197e7f7ba90786f8fc06e75e6c8e6 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/PluginManagerInterface.php @@ -0,0 +1,31 @@ +<?php +/** + * @file + * Definition of Drupal\Component\Plugin\PluginManagerInterface + */ + +namespace Drupal\Component\Plugin; + +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\Component\Plugin\Factory\FactoryInterface; +use Drupal\Component\Plugin\Mapper\MapperInterface; + +/** + * Interface implemented by plugin managers. + * + * There are no explicit methods on the manager interface. Instead plugin + * managers broker the interactions of the different plugin components, and + * therefore, must implement each component interface, which is enforced by + * this interface extending all of the component ones. + * + * While a plugin manager may directly implement these interface methods with + * custom logic, it is expected to be more common for plugin managers to proxy + * the method invocations to the respective components, and directly implement + * only the additional functionality needed by the specific pluggable system. + * To follow this pattern, plugin managers can extend from the PluginManagerBase + * class, which contains the proxying logic. + * + * @see Drupal\Component\Plugin\PluginManagerBase + */ +interface PluginManagerInterface extends DiscoveryInterface, FactoryInterface, MapperInterface { +} diff --git a/core/lib/Drupal/Core/Plugin/Discovery/CacheDecorator.php b/core/lib/Drupal/Core/Plugin/Discovery/CacheDecorator.php new file mode 100644 index 0000000000000000000000000000000000000000..49a0d2863d9f8e55ec6747737bb1cf935c4211dd --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Discovery/CacheDecorator.php @@ -0,0 +1,124 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Plugin\Discovery\CacheDecorator. + */ + +namespace Drupal\Core\Plugin\Discovery; + +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; + +/** + * Enables static and persistent caching of discovered plugin definitions. + */ +class CacheDecorator implements DiscoveryInterface { + + /** + * The cache key used to store the definition list. + * + * @var string + */ + protected $cacheKey; + + /** + * The cache bin used to store the definition list. + * + * @var string + */ + protected $cacheBin; + + /** + * The plugin definitions of the decorated discovery class. + * + * @var array + */ + protected $definitions; + + /** + * The Discovery object being decorated. + * + * @var Drupal\Component\Plugin\Discovery\DiscoveryInterface + */ + protected $decorated; + + /** + * Constructs a Drupal\Core\Plugin\Discovery\CacheDecorator object. + * + * It uses the DiscoveryInterface object it should decorate. + * + * @param Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated + * The object implementing DiscoveryInterface that is being decorated. + * @param string $cache_key + * The cache identifier used for storage of the definition list. + * @param string $cache_bin + * The cache bin used for storage and retrieval of the definition list. + */ + public function __construct(DiscoveryInterface $decorated, $cache_key, $cache_bin = 'default') { + $this->decorated = $decorated; + $this->cacheKey = $cache_key; + $this->cacheBin = $cache_bin; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DicoveryInterface::getDefinition(). + */ + public function getDefinition($plugin_id) { + $definitions = $this->getCachedDefinitions(); + if (isset($definitions)) { + $definition = isset($definitions[$plugin_id]) ? $definitions[$plugin_id] : NULL; + } + else { + $definition = $this->decorated->getDefinition($plugin_id); + } + return $definition; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DicoveryInterface::getDefinitions(). + */ + public function getDefinitions() { + $definitions = $this->getCachedDefinitions(); + if (!isset($definitions)) { + $definitions = $this->decorated->getDefinitions(); + $this->setCachedDefinitions($definitions); + } + return $definitions; + } + + /** + * Returns the cached plugin definitions of the decorated discovery class. + * + * @return mixed + * On success this will return an array of plugin definitions. On failure + * this should return NULL, indicating to other methods that this has not + * yet been defined. Success with no values should return as an empty array + * and would actually be returned by the getDefinitions() method. + */ + protected function getCachedDefinitions() { + if (!isset($this->definitions) && isset($this->cacheKey) && $cache = cache($this->cacheBin)->get($this->cacheKey)) { + $this->definitions = $cache->data; + } + return $this->definitions; + } + + /** + * Sets a cache of plugin definitions for the decorated discovery class. + * + * @param array $definitions + * List of definitions to store in cache. + */ + protected function setCachedDefinitions($definitions) { + if (isset($this->cacheKey)) { + cache($this->cacheBin)->set($this->cacheKey, $definitions); + } + $this->definitions = $definitions; + } + + /** + * Passes through all unknown calls onto the decorated object. + */ + public function __call($method, $args) { + return call_user_func_array(array($this->decorated, $method), $args); + } +} diff --git a/core/lib/Drupal/Core/Plugin/Discovery/HookDiscovery.php b/core/lib/Drupal/Core/Plugin/Discovery/HookDiscovery.php new file mode 100644 index 0000000000000000000000000000000000000000..3ff05f9197ff8326384c8b53b56040a18b9799c0 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Discovery/HookDiscovery.php @@ -0,0 +1,57 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Plugin\Discovery\HookDiscovery. + */ + +namespace Drupal\Core\Plugin\Discovery; + +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; + +/** + * Provides a hook-based plugin discovery class. + */ +class HookDiscovery implements DiscoveryInterface { + + /** + * The name of the hook that will be implemented by this discovery instance. + * + * @var string + */ + protected $hook; + + /** + * Constructs a Drupal\Core\Plugin\Discovery\HookDiscovery object. + * + * @param string $hook + * The Drupal hook that a module can implement in order to interface to + * this discovery class. + */ + function __construct($hook) { + $this->hook = $hook; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DicoveryInterface::getDefinition(). + */ + public function getDefinition($plugin_id) { + $plugins = $this->getDefinitions(); + return isset($plugins[$plugin_id]) ? $plugins[$plugin_id] : array(); + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DicoveryInterface::getDefinitions(). + */ + public function getDefinitions() { + foreach (module_implements($this->hook) as $module) { + $function = $module . '_' . $this->hook; + foreach ($function() as $plugin_id => $definition) { + $definition['module'] = $module; + $definitions[$plugin_id] = $definition; + } + } + drupal_alter($this->hook, $definitions); + return $definitions; + } +} diff --git a/core/modules/aggregator/aggregator.admin.inc b/core/modules/aggregator/aggregator.admin.inc index 7759750ba3a9699c1887cb1558931bd4c462c3fb..0ee5ee61f197171cd6a309751e594b87b4094a6b 100644 --- a/core/modules/aggregator/aggregator.admin.inc +++ b/core/modules/aggregator/aggregator.admin.inc @@ -6,6 +6,7 @@ */ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Drupal\aggregator\Plugin\FetcherManager; /** * Page callback: Displays the aggregator administration page. @@ -442,16 +443,11 @@ function aggregator_admin_form($form, $form_state) { aggregator_sanitize_configuration(); // Get all available fetchers. - $fetchers = module_implements('aggregator_fetch'); - foreach ($fetchers as $k => $module) { - if ($info = module_invoke($module, 'aggregator_fetch_info')) { - $label = $info['title'] . ' <span class="description">' . $info['description'] . '</span>'; - } - else { - $label = $module; - } - unset($fetchers[$k]); - $fetchers[$module] = $label; + $fetcher_manager = new FetcherManager(); + $fetchers = array(); + foreach ($fetcher_manager->getDefinitions() as $id => $definition) { + $label = $definition['title'] . ' <span class="description">' . $definition['description'] . '</span>'; + $fetchers[$id] = $label; } // Get all available parsers. diff --git a/core/modules/aggregator/aggregator.api.php b/core/modules/aggregator/aggregator.api.php index 0f708eb85ef3423825763aafcd1c4f6622b4063d..3d7f4996fd05efcbb19e9ac6f6aaeab0a04335a3 100644 --- a/core/modules/aggregator/aggregator.api.php +++ b/core/modules/aggregator/aggregator.api.php @@ -11,58 +11,29 @@ */ /** - * Create an alternative fetcher for aggregator.module. - * - * A fetcher downloads feed data to a Drupal site. The fetcher is called at the - * first of the three aggregation stages: first, data is downloaded by the - * active fetcher; second, it is converted to a common format by the active - * parser; and finally, it is passed to all active processors, which manipulate - * or store the data. - * - * Modules that define this hook can be set as active fetcher on - * admin/config/services/aggregator. Only one fetcher can be active at a time. - * - * @param $feed - * A feed object representing the resource to be downloaded. $feed->url - * contains the link to the feed. Download the data at the URL and expose it - * to other modules by attaching it to $feed->source_string. - * - * @return - * TRUE if fetching was successful, FALSE otherwise. - * - * @see hook_aggregator_fetch_info() - * @see hook_aggregator_parse() - * @see hook_aggregator_process() - * - * @ingroup aggregator - */ -function hook_aggregator_fetch($feed) { - $feed->source_string = mymodule_fetch($feed->url); -} - -/** - * Specify the title and short description of your fetcher. + * Specify the class, title, and short description of your fetcher plugins. * * The title and the description provided are shown on - * admin/config/services/aggregator among other places. Use as title the human - * readable name of the fetcher and as description a brief (40 to 80 characters) - * explanation of the fetcher's functionality. - * - * This hook is only called if your module implements hook_aggregator_fetch(). - * If this hook is not implemented aggregator will use your module's file name - * as title and there will be no description. + * admin/config/services/aggregator among other places. * * @return - * An associative array defining a title and a description string. - * - * @see hook_aggregator_fetch() + * An associative array whose keys define the fetcher id and whose values + * contain the fetcher definitions. Each fetcher definition is itself an + * associative array, with the following key-value pairs: + * - class: (required) The PHP class containing the fetcher implementation. + * - title: (required) A human readable name of the fetcher. + * - description: (required) A brief (40 to 80 characters) explanation of the + * fetcher's functionality. * * @ingroup aggregator */ function hook_aggregator_fetch_info() { return array( - 'title' => t('Default fetcher'), - 'description' => t('Default fetcher for resources available by URL.'), + 'aggregator' => array( + 'class' => 'Drupal\aggregator\Plugin\aggregator\fetcher\DefaultFetcher', + 'title' => t('Default fetcher'), + 'description' => t('Downloads data from a URL using Drupal\'s HTTP request handler.'), + ), ); } diff --git a/core/modules/aggregator/aggregator.module b/core/modules/aggregator/aggregator.module index 0626f726e3c80582c0549d9066ec802f7bc54036..2d37b73de9bde235ed6cc71c3662b27e7aad817a 100644 --- a/core/modules/aggregator/aggregator.module +++ b/core/modules/aggregator/aggregator.module @@ -5,6 +5,8 @@ * Used to aggregate syndicated content (RSS, RDF, and Atom). */ +use Drupal\aggregator\Plugin\FetcherManager; + /** * Denotes that a feed's items should never expire. */ @@ -594,19 +596,18 @@ function aggregator_remove($feed) { * An array containing the fetcher, parser, and processors. */ function _aggregator_get_variables() { - // Fetch the feed. $fetcher = variable_get('aggregator_fetcher', 'aggregator'); - if ($fetcher == 'aggregator') { - include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'aggregator') . '/aggregator.fetcher.inc'; - } + $parser = variable_get('aggregator_parser', 'aggregator'); if ($parser == 'aggregator') { include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'aggregator') . '/aggregator.parser.inc'; } + $processors = variable_get('aggregator_processors', array('aggregator')); if (in_array('aggregator', $processors)) { include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'aggregator') . '/aggregator.processor.inc'; } + return array($fetcher, $parser, $processors); } @@ -620,9 +621,16 @@ function aggregator_refresh($feed) { // Store feed URL to track changes. $feed_url = $feed->url; - // Fetch the feed. list($fetcher, $parser, $processors) = _aggregator_get_variables(); - $success = module_invoke($fetcher, 'aggregator_fetch', $feed); + + // Fetch the feed. + $fetcher_manager = new FetcherManager(); + try { + $success = $fetcher_manager->createInstance($fetcher)->fetch($feed); + } + catch (PluginException $e) { + $success = FALSE; + } // We store the hash of feed data in the database. When refreshing a // feed we compare stored hash and new hash calculated from downloaded @@ -788,6 +796,19 @@ function _aggregator_items($count) { return format_plural($count, '1 item', '@count items'); } +/** + * Implements hook_aggregator_fetch_info(). + */ +function aggregator_aggregator_fetch_info() { + return array( + 'aggregator' => array( + 'class' => 'Drupal\aggregator\Plugin\aggregator\fetcher\DefaultFetcher', + 'title' => t('Default fetcher'), + 'description' => t('Downloads data from a URL using Drupal\'s HTTP request handler.'), + ), + ); +} + /** * Implements hook_preprocess_HOOK() for block.tpl.php. */ diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/FetcherInterface.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/FetcherInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..39fc19df2aee5a89e01e1f8de1d3fe709eae81f7 --- /dev/null +++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/FetcherInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * @file + * Definition of Drupal\aggregator\Plugin\FetcherInterface. + */ + +namespace Drupal\aggregator\Plugin; + +/** + * Defines an interface for aggregator fetcher implementations. + * + * A fetcher downloads feed data to a Drupal site. The fetcher is called at the + * first of the three aggregation stages: first, data is downloaded by the + * active fetcher; second, it is converted to a common format by the active + * parser; and finally, it is passed to all active processors, which manipulate + * or store the data. + */ +interface FetcherInterface { + + /** + * Downloads feed data. + * + * @param $feed + * A feed object representing the resource to be downloaded. $feed->url + * contains the link to the feed. Download the data at the URL and expose it + * to other modules by attaching it to $feed->source_string. + * + * @return + * TRUE if fetching was successful, FALSE otherwise. + */ + public function fetch(&$feed); + +} diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/FetcherManager.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/FetcherManager.php new file mode 100644 index 0000000000000000000000000000000000000000..ae24934d1934305cf5af88a90554691a1b2def24 --- /dev/null +++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/FetcherManager.php @@ -0,0 +1,23 @@ +<?php + +/** + * @file + * Definition of Drupal\aggregator\Plugin\FetcherManager. + */ + +namespace Drupal\aggregator\Plugin; + +use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Core\Plugin\Discovery\HookDiscovery; +use Drupal\Component\Plugin\Factory\DefaultFactory; + +/** + * Manages aggregator fetcher plugins. + */ +class FetcherManager extends PluginManagerBase { + + public function __construct() { + $this->discovery = new HookDiscovery('aggregator_fetch_info'); + $this->factory = new DefaultFactory($this->discovery); + } +} diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/aggregator/fetcher/DefaultFetcher.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/aggregator/fetcher/DefaultFetcher.php new file mode 100644 index 0000000000000000000000000000000000000000..e4b663ba47bb1d5ac32d1aad34421da1dddee30b --- /dev/null +++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/aggregator/fetcher/DefaultFetcher.php @@ -0,0 +1,63 @@ +<?php + +/** + * @file + * Definition of Drupal\aggregator\Plugin\aggregator\fetcher\DefaultFetcher. + */ + +namespace Drupal\aggregator\Plugin\aggregator\fetcher; + +use Drupal\aggregator\Plugin\FetcherInterface; + +/** + * Defines a default fetcher implementation. + * + * Uses drupal_http_request() to download the feed. + */ +class DefaultFetcher implements FetcherInterface { + + /** + * Implements Drupal\aggregator\Plugin\FetcherInterface::fetch(). + */ + function fetch(&$feed) { + $feed->source_string = FALSE; + + // Generate conditional GET headers. + $headers = array(); + if ($feed->etag) { + $headers['If-None-Match'] = $feed->etag; + } + if ($feed->modified) { + $headers['If-Modified-Since'] = gmdate(DATE_RFC1123, $feed->modified); + } + + // Request feed. + $result = drupal_http_request($feed->url, array('headers' => $headers)); + + // Process HTTP response code. + switch ($result->code) { + case 304: + break; + case 301: + $feed->url = $result->redirect_url; + // Do not break here. + case 200: + case 302: + case 307: + if (!isset($result->data)) { + $result->data = ''; + } + if (!isset($result->headers)) { + $result->headers = array(); + } + $feed->source_string = $result->data; + $feed->http_headers = $result->headers; + break; + default: + watchdog('aggregator', 'The feed from %site seems to be broken due to "%error".', array('%site' => $feed->title, '%error' => $result->code . ' ' . $result->error), WATCHDOG_WARNING); + drupal_set_message(t('The feed from %site seems to be broken because of error "%error".', array('%site' => $feed->title, '%error' => $result->code . ' ' . $result->error))); + } + + return !($feed->source_string === FALSE); + } +} diff --git a/core/modules/simpletest/simpletest.info b/core/modules/simpletest/simpletest.info index 46f00c3a32bb4b39af9c1b5f5c9eb06bd9886484..6e18c42bd3afeec7d5de00f8872ec073db066e46 100644 --- a/core/modules/simpletest/simpletest.info +++ b/core/modules/simpletest/simpletest.info @@ -4,3 +4,4 @@ package = Core version = VERSION core = 8.x configure = admin/config/development/testing/settings + diff --git a/core/modules/system/lib/Drupal/system/Tests/Plugin/DerivativeTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/DerivativeTest.php new file mode 100644 index 0000000000000000000000000000000000000000..226b28d6f290c537def3ff2bfe5cf2a75d98a9f5 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/DerivativeTest.php @@ -0,0 +1,42 @@ +<?php + +/** + * @file + * Definition of Drupal\system\Tests\Plugin\DerivativeTest. + */ + +namespace Drupal\system\Tests\Plugin; + +/** + * Tests that derivative plugins are correctly discovered. + */ +class DerivativeTest extends PluginTestBase { + + public static function getInfo() { + return array( + 'name' => 'Derivative Discovery', + 'description' => 'Tests that derivative plugins are correctly discovered.', + 'group' => 'Plugin API', + ); + } + + /** + * Tests getDefinitions() and getDefinition() with a derivativeDecorator. + */ + function testDerivativeDecorator() { + // Ensure that getDefinitions() returns the expected definitions. + $this->assertIdentical($this->mockBlockManager->getDefinitions(), $this->mockBlockExpectedDefinitions); + + // Ensure that getDefinition() returns the expected definition. + foreach ($this->mockBlockExpectedDefinitions as $id => $definition) { + $this->assertIdentical($this->mockBlockManager->getDefinition($id), $definition); + } + + // Ensure that NULL is returned as the definition of a non-existing base + // plugin, a non-existing derivative plugin, or a base plugin that may not + // be used without deriving. + $this->assertIdentical($this->mockBlockManager->getDefinition('non_existing'), NULL, 'NULL returned as the definition of a non-existing base plugin.'); + $this->assertIdentical($this->mockBlockManager->getDefinition('menu:non_existing'), NULL, 'NULL returned as the definition of a non-existing derivative plugin.'); + $this->assertIdentical($this->mockBlockManager->getDefinition('menu'), NULL, 'NULL returned as the definition of a base plugin that may not be used without deriving.'); + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Plugin/DiscoveryTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/DiscoveryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..32637c672887204c846c7af8c43b18978a217b0a --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/DiscoveryTest.php @@ -0,0 +1,38 @@ +<?php + +/** + * @file + * Definition of Drupal\system\Tests\Plugin\DiscoveryTest. + */ + +namespace Drupal\system\Tests\Plugin; + +/** + * Tests that plugins are correctly discovered. + */ +class DiscoveryTest extends PluginTestBase { + + public static function getInfo() { + return array( + 'name' => 'Discovery', + 'description' => 'Tests that plugins are correctly discovered.', + 'group' => 'Plugin API', + ); + } + + /** + * Tests getDefinitions() and getDefinition(). + */ + function testDiscoveryInterface() { + // Ensure that getDefinitions() returns the expected definitions. + $this->assertIdentical($this->testPluginManager->getDefinitions(), $this->testPluginExpectedDefinitions); + + // Ensure that getDefinition() returns the expected definition. + foreach ($this->testPluginExpectedDefinitions as $id => $definition) { + $this->assertIdentical($this->testPluginManager->getDefinition($id), $definition); + } + + // Ensure that NULL is returned as the definition of a non-existing plugin. + $this->assertIdentical($this->testPluginManager->getDefinition('non_existing'), NULL, 'NULL returned as the definition of a non-existing base plugin.'); + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Plugin/FactoryTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/FactoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..55c160f974ef29c79c35269a536630b70c82ede0 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/FactoryTest.php @@ -0,0 +1,86 @@ +<?php + +/** + * @file + * Definition of Drupal\system\Tests\Plugin\FactoryTest. + */ + +namespace Drupal\system\Tests\Plugin; + +use Drupal\Component\Plugin\Exception\ExceptionInterface; +use Exception; + +/** + * Tests that plugins are correctly instantiated. + */ +class FactoryTest extends PluginTestBase { + + public static function getInfo() { + return array( + 'name' => 'Factory', + 'description' => 'Tests that plugins are correctly instantiated.', + 'group' => 'Plugin API', + ); + } + + /** + * Test that DefaultFactory can create a plugin instance. + */ + function testDefaultFactory() { + // Ensure a non-derivative plugin can be instantiated. + $plugin = $this->testPluginManager->createInstance('user_login', array('title' => 'Please enter your login name and password')); + $this->assertIdentical(get_class($plugin), 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserLoginBlock', 'Correct plugin class instantiated with default factory.'); + $this->assertIdentical($plugin->getTitle(), 'Please enter your login name and password', 'Plugin instance correctly configured.'); + + // Ensure that attempting to instantiate non-existing plugins throws a + // PluginException. + try { + $this->testPluginManager->createInstance('non_existing'); + $this->fail('Drupal\Component\Plugin\Exception\ExceptionInterface expected'); + } + catch (ExceptionInterface $e) { + $this->pass('Drupal\Component\Plugin\Exception\ExceptionInterface expected and caught.'); + } + catch (Exception $e) { + $this->fail('Drupal\Component\Plugin\Exception\ExceptionInterface expected, but ' . get_class($e) . ' was thrown.'); + } + } + + /** + * Test that the Reflection factory can create a plugin instance. + * + * The mock plugin classes use different values for their constructors + * allowing us to test the reflection capabilities as well. + * + * We use derivative classes here because the block test type has the + * reflection factory and it provides some additional variety in plugin + * object creation. + */ + function testReflectionFactory() { + // Ensure a non-derivative plugin can be instantiated. + $plugin = $this->mockBlockManager->createInstance('user_login', array('title' => 'Please enter your login name and password')); + $this->assertIdentical(get_class($plugin), 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserLoginBlock', 'Correct plugin class instantiated.'); + $this->assertIdentical($plugin->getTitle(), 'Please enter your login name and password', 'Plugin instance correctly configured.'); + + // Ensure a derivative plugin can be instantiated. + $plugin = $this->mockBlockManager->createInstance('menu:main_menu', array('depth' => 2)); + $this->assertIdentical($plugin->getContent(), '<ul><li>1<ul><li>1.1</li></ul></li></ul>', 'Derived plugin instance correctly instantiated and configured.'); + + // Ensure that attempting to instantiate non-existing plugins throws a + // PluginException. Test this for a non-existing base plugin, a non-existing + // derivative plugin, and a base plugin that may not be used without + // deriving. + foreach (array('non_existing', 'menu:non_existing', 'menu') as $invalid_id) { + try { + $this->mockBlockManager->createInstance($invalid_id); + $this->fail('Drupal\Component\Plugin\Exception\ExceptionInterface expected'); + } + catch (ExceptionInterface $e) { + $this->pass('Drupal\Component\Plugin\Exception\ExceptionInterface expected and caught.'); + } + catch (Exception $e) { + $this->fail('An unexpected Exception of type "' . get_class($e) . '" was thrown with message ' . $e->getMessage()); + } + } + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Plugin/InspectionTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/InspectionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..dcdaa34f30bc19d1b526ca73b112b6eadf6abbc7 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/InspectionTest.php @@ -0,0 +1,41 @@ +<?php + +/** + * @file + * Definition of Drupal\system\Tests\Plugin\InspectionTest + */ + +namespace Drupal\system\Tests\Plugin; + +/** + * Tests that plugins implementing PluginInspectionInterface are inspectable. + */ +class InspectionTest extends PluginTestBase { + + public static function getInfo() { + return array( + 'name' => 'Inspection', + 'description' => 'Tests that plugins implementing PluginInspectionInterface are inspectable.', + 'group' => 'Plugin API', + ); + } + + /** + * Ensure the test plugins correctly implement getPluginId() and getDefinition(). + */ + function testInspection() { + foreach (array('user_login') as $id) { + $plugin = $this->testPluginManager->createInstance($id); + $this->assertIdentical($plugin->getPluginId(), $id); + $this->assertIdentical($plugin->getDefinition(), $this->testPluginExpectedDefinitions[$id]); + } + // Skip the 'menu' derived blocks, because MockMenuBlock does not implement + // PluginInspectionInterface. The others do by extending PluginBase. + foreach (array('user_login', 'layout') as $id) { + $plugin = $this->mockBlockManager->createInstance($id); + $this->assertIdentical($plugin->getPluginId(), $id); + $this->assertIdentical($plugin->getDefinition(), $this->mockBlockExpectedDefinitions[$id]); + } + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Plugin/PluginTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/PluginTestBase.php new file mode 100644 index 0000000000000000000000000000000000000000..5595e28bce5d2d243a3f7392b40ee5f83aad2e6c --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/PluginTestBase.php @@ -0,0 +1,71 @@ +<?php + +/** + * @file + * Definition of Drupal\system\Tests\Plugin\PluginTestBase. + */ + +namespace Drupal\system\Tests\Plugin; + +use Drupal\simpletest\UnitTestBase; +use Drupal\plugin_test\Plugin\TestPluginManager; +use Drupal\plugin_test\Plugin\MockBlockManager; + +/** + * Base class for Plugin API unit tests. + */ +abstract class PluginTestBase extends UnitTestBase { + protected $testPluginManager; + protected $testPluginExpectedDefinitions; + protected $mockBlockManager; + protected $mockBlockExpectedDefinitions; + + public function setUp() { + parent::setUp(); + + // Real modules implementing plugin types may expose a module-specific API + // for retrieving each type's plugin manager, or make them available in + // Drupal's dependency injection container, but for unit testing, we get + // the managers directly. + // - TestPluginManager is a bare bones manager with no support for + // derivatives, and uses DefaultFactory for plugin instantiation. + // - MockBlockManager is used for testing more advanced functionality such + // as derivatives and ReflectionFactory. + $this->testPluginManager = new TestPluginManager(); + $this->mockBlockManager = new MockBlockManager(); + + // The expected plugin definitions within each manager. Several tests assert + // that these plugins and their definitions are found and returned by the + // necessary API functions. + // @see TestPluginManager::_construct(). + // @see MockBlockManager::_construct(). + $this->testPluginExpectedDefinitions = array( + 'user_login' => array( + 'label' => 'User login', + 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserLoginBlock', + ), + ); + $this->mockBlockExpectedDefinitions = array( + 'user_login' => array( + 'label' => 'User login', + 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserLoginBlock', + ), + 'menu:main_menu' => array( + 'label' => 'Main menu', + 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlock', + ), + 'menu:navigation' => array( + 'label' => 'Navigation', + 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlock', + ), + 'layout' => array( + 'label' => 'Layout', + 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockLayoutBlock', + ), + 'layout:foo' => array( + 'label' => 'Layout Foo', + 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockLayoutBlock', + ), + ); + } +} diff --git a/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/MockBlockManager.php b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/MockBlockManager.php new file mode 100644 index 0000000000000000000000000000000000000000..14484f5c69c32fa93c6498ade5f9a2422d11320d --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/MockBlockManager.php @@ -0,0 +1,80 @@ +<?php + +/** + * @file + * Definition of Drupal\plugin_test\Plugin\MockBlockManager. + */ + +namespace Drupal\plugin_test\Plugin; + +use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Component\Plugin\Discovery\StaticDiscovery; +use Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator; +use Drupal\Component\Plugin\Factory\ReflectionFactory; + +/** + * Defines a plugin manager used by Plugin API derivative unit tests. + */ +class MockBlockManager extends PluginManagerBase { + public function __construct() { + + // Create the object that can be used to return definitions for all the + // plugins available for this type. Most real plugin managers use a richer + // discovery implementation, but StaticDiscovery lets us add some simple + // mock plugins for unit testing. + $this->discovery = new StaticDiscovery(); + + // Derivative plugins are plugins that are derived from a base plugin + // definition and some site configuration (examples below). To allow for + // such plugins, we add the DerivativeDiscoveryDecorator to our discovery + // object. + $this->discovery = new DerivativeDiscoveryDecorator($this->discovery); + + // The plugin definitions that follow are based on work that is in progress + // for the Drupal 8 Blocks and Layouts initiative + // (http://groups.drupal.org/node/213563). As stated above, we set + // definitions here, because this is for unit testing. Real plugin managers + // use a discovery implementation that allows for any module to add new + // plugins to the system. + + // A simple plugin: the user login block. + $this->discovery->setDefinition('user_login', array( + 'label' => 'User login', + 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserLoginBlock', + )); + + // A plugin that requires derivatives: the menu block plugin. We do not want + // a generic "Menu" block showing up in the Block administration UI. + // Instead, we want a block for each menu, but the number of menus in the + // system and each one's title is user configurable. The + // MockMenuBlockDeriver class ensures that only derivatives, and not the + // base plugin, are available to the system. + $this->discovery->setDefinition('menu', array( + 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlock', + 'derivative' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlockDeriver', + )); + + // A block plugin that can optionally be derived: the layout block plugin. + // A layout is a special kind of block into which other blocks can be + // placed. We want both a generic "Layout" block available in the Block + // administration UI as well as additional user-created custom layouts. The + // MockLayoutBlockDeriver class ensures that both the base plugin and the + // derivatives are available to the system. + $this->discovery->setDefinition('layout', array( + 'label' => 'Layout', + 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockLayoutBlock', + 'derivative' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockLayoutBlockDeriver', + )); + + // In addition to finding all of the plugins available for a type, a plugin + // type must also be able to create instances of that plugin. For example, a + // specific instance of a "Main menu" menu block, configured to show just + // the top-level of links. To handle plugin instantiation, plugin managers + // can use one of the factory classes included with the plugin system, or + // create their own. ReflectionFactory is a general purpose, flexible + // factory suitable for many kinds of plugin types. Factories need access to + // the plugin definitions (e.g., since that's where the plugin's class is + // specified), so we provide it the discovery object. + $this->factory = new ReflectionFactory($this->discovery); + } +} diff --git a/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/TestPluginManager.php b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/TestPluginManager.php new file mode 100644 index 0000000000000000000000000000000000000000..85fabeb4698f6a28f56452d520e3c14ebe3656c0 --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/TestPluginManager.php @@ -0,0 +1,44 @@ +<?php + +/** + * @file + * Definition of Drupal\plugin_test\Plugin\TestPluginManager. + */ + +namespace Drupal\plugin_test\Plugin; + +use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Component\Plugin\Discovery\StaticDiscovery; +use Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator; +use Drupal\Component\Plugin\Factory\DefaultFactory; + +/** + * Defines a plugin manager used by Plugin API unit tests. + */ +class TestPluginManager extends PluginManagerBase { + public function __construct() { + + // Create the object that can be used to return definitions for all the + // plugins available for this type. Most real plugin managers use a richer + // discovery implementation, but StaticDiscovery lets us add some simple + // mock plugins for unit testing. + $this->discovery = new StaticDiscovery(); + + // A simple plugin: a mock user login block. + $this->discovery->setDefinition('user_login', array( + 'label' => 'User login', + 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserLoginBlock', + )); + + // In addition to finding all of the plugins available for a type, a plugin + // type must also be able to create instances of that plugin. For example, a + // specific instance of a "User login" block, configured with a custom + // title. To handle plugin instantiation, plugin managers can use one of the + // factory classes included with the plugin system, or create their own. + // DefaultFactory is a simple, general purpose factory suitable for + // many kinds of plugin types. Factories need access to the plugin + // definitions (e.g., since that's where the plugin's class is specified), + // so we provide it the discovery object. + $this->factory = new DefaultFactory($this->discovery); + } +} diff --git a/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockLayoutBlock.php b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockLayoutBlock.php new file mode 100644 index 0000000000000000000000000000000000000000..4e8a2f562060069906bd500e5476356ca7eee792 --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockLayoutBlock.php @@ -0,0 +1,22 @@ +<?php + +/** + * @file + * Definition of Drupal\plugin_test\Plugin\plugin_test\mock_block\MockLayoutBlock. + */ + +namespace Drupal\plugin_test\Plugin\plugin_test\mock_block; + +use Drupal\Component\Plugin\PluginBase; + +/** + * Mock implementation of a layout block plugin used by Plugin API unit tests. + * + * No implementation here as there are no tests for this yet. + * + * @see Drupal\plugin_test\Plugin\MockBlockManager + * @see Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserLoginBlock + * @see Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlock + */ +class MockLayoutBlock extends PluginBase { +} diff --git a/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockLayoutBlockDeriver.php b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockLayoutBlockDeriver.php new file mode 100644 index 0000000000000000000000000000000000000000..c654bd04b8d0cca734f21e567b7eda49477abe7b --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockLayoutBlockDeriver.php @@ -0,0 +1,55 @@ +<?php + +/** + * @file + * Definition of Drupal\plugin_test\Plugin\plugin_test\mock_block\MockLayoutBlockDeriver. + */ + +namespace Drupal\plugin_test\Plugin\plugin_test\mock_block; + +use Drupal\Component\Plugin\Derivative\DerivativeInterface; + +/** + * Mock implementation of DerivativeInterface for the mock layout block plugin. + * + * @see Drupal\plugin_test\Plugin\MockBlockManager + */ +class MockLayoutBlockDeriver implements DerivativeInterface { + + /** + * Implements Drupal\Component\Plugin\Derivative\DerivativeInterface::getDerivativeDefinition(). + */ + public function getDerivativeDefinition($derivative_id, array $base_plugin_definition) { + $derivatives = $this->getDerivativeDefinitions($base_plugin_definition); + if (isset($derivatives[$derivative_id])) { + return $derivatives[$derivative_id]; + } + } + + /** + * Implements Drupal\Component\Plugin\Derivative\DerivativeInterface::getDerivativeDefinitions(). + */ + public function getDerivativeDefinitions(array $base_plugin_definition) { + // This isn't strictly necessary, but it helps reduce clutter in + // DerivativePluginTest::testDerivativeDecorator()'s $expected variable. + // Since derivative definitions don't need further deriving, we remove this + // key from the returned definitions. + unset($base_plugin_definition['derivative']); + + $derivatives = array( + // Adding a NULL key signifies that the base plugin may also be used in + // addition to the derivatives. In this case, we allow the administrator + // to add a generic layout block to the page. + NULL => $base_plugin_definition, + + // We also allow them to add a customized one. Here, we just mock the + // customized one, but in a real implementation, this would be fetched + // from some config() object. + 'foo' => array( + 'label' => 'Layout Foo', + ) + $base_plugin_definition, + ); + + return $derivatives; + } +} diff --git a/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockMenuBlock.php b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockMenuBlock.php new file mode 100644 index 0000000000000000000000000000000000000000..84ec835bfcdeef9bf51b36bc5f32a09d86435a07 --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockMenuBlock.php @@ -0,0 +1,54 @@ +<?php + +/** + * @file + * Definition of Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlock. + */ + +namespace Drupal\plugin_test\Plugin\plugin_test\mock_block; + +use Drupal\Component\Plugin\PluginBase; + +/** + * Mock implementation of a menu block plugin used by Plugin API unit tests. + * + * @see Drupal\plugin_test\Plugin\MockBlockManager + */ +class MockMenuBlock { + + /** + * The title to display when rendering this block instance. + * + * @var string + */ + protected $title; + + /** + * The number of menu levels deep to render. + * + * @var integer + */ + protected $depth; + + public function __construct($title = '', $depth = 0) { + $this->title = $title; + $this->depth = $depth; + } + + /** + * Returns the content to display. + */ + public function getContent() { + // Since this is a mock object, we just return some HTML of the desired + // nesting level. For depth=2, this returns: + // '<ul><li>1<ul><li>1.1</li></ul></li></ul>'. + $content = ''; + for ($i=0; $i < $this->depth; $i++) { + $content .= '<ul><li>' . implode('.', array_fill(0, $i+1, '1')); + } + for ($i=0; $i < $this->depth; $i++) { + $content .= '</li></ul>'; + } + return $content; + } +} diff --git a/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockMenuBlockDeriver.php b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockMenuBlockDeriver.php new file mode 100644 index 0000000000000000000000000000000000000000..aa84202bbfc231adc39ff7c78c16024dcf3e5c03 --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockMenuBlockDeriver.php @@ -0,0 +1,53 @@ +<?php + +/** + * @file + * Definition of Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlockDeriver. + */ + +namespace Drupal\plugin_test\Plugin\plugin_test\mock_block; + +use Drupal\Component\Plugin\Derivative\DerivativeInterface; + +/** + * Mock implementation of DerivativeInterface for the mock menu block plugin. + * + * @see Drupal\plugin_test\Plugin\MockBlockManager + */ +class MockMenuBlockDeriver implements DerivativeInterface { + + /** + * Implements Drupal\Component\Plugin\Derivative\DerivativeInterface::getDerivativeDefinition(). + */ + public function getDerivativeDefinition($derivative_id, array $base_plugin_definition) { + $derivatives = $this->getDerivativeDefinitions($base_plugin_definition); + if (isset($derivatives[$derivative_id])) { + return $derivatives[$derivative_id]; + } + } + + /** + * Implements Drupal\Component\Plugin\Derivative\DerivativeInterface::getDerivativeDefinitions(). + */ + public function getDerivativeDefinitions(array $base_plugin_definition) { + // This isn't strictly necessary, but it helps reduce clutter in + // DerivativePluginTest::testDerivativeDecorator()'s $expected variable. + // Since derivative definitions don't need further deriving, we remove this + // key from the returned definitions. + unset($base_plugin_definition['derivative']); + + // Here, we create some mock menu block definitions for menus that might + // exist in a typical Drupal site. In a real implementation, we would query + // Drupal's configuration to find out which menus actually exist. + $derivatives = array( + 'main_menu' => array( + 'label' => 'Main menu', + ) + $base_plugin_definition, + 'navigation' => array( + 'label' => 'Navigation', + ) + $base_plugin_definition, + ); + + return $derivatives; + } +} diff --git a/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockUserLoginBlock.php b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockUserLoginBlock.php new file mode 100644 index 0000000000000000000000000000000000000000..e83e8cf524350723c89cd00f6dfb4fb589b896ad --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockUserLoginBlock.php @@ -0,0 +1,35 @@ +<?php + +/** + * @file + * Definition of Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserLoginBlock. + */ + +namespace Drupal\plugin_test\Plugin\plugin_test\mock_block; + +use Drupal\Component\Plugin\PluginBase; +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; + +/** + * Mock implementation of a login block plugin used by Plugin API unit tests. + * + * @see Drupal\plugin_test\Plugin\MockBlockManager + */ +class MockUserLoginBlock extends PluginBase { + + /** + * The title to display when rendering this block instance. + * + * @var string + */ + protected $title; + + public function __construct(array $configuration, $plugin_id, DiscoveryInterface $discovery) { + parent::__construct($configuration, $plugin_id, $discovery); + $this->title = isset($configuration['title']) ? $configuration['title'] : ''; + } + + public function getTitle() { + return $this->title; + } +} diff --git a/core/modules/system/tests/modules/plugin_test/plugin_test.info b/core/modules/system/tests/modules/plugin_test/plugin_test.info new file mode 100644 index 0000000000000000000000000000000000000000..406e02fd98440a6c633bdb0dfd8963c718168bb5 --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/plugin_test.info @@ -0,0 +1,6 @@ +name = "Plugin Test Support" +description = "Test that plugins can provide plugins and provide namespace discovery for plugin test implementations." +package = Testing +version = VERSION +core = 8.x +hidden = TRUE diff --git a/core/modules/system/tests/modules/plugin_test/plugin_test.module b/core/modules/system/tests/modules/plugin_test/plugin_test.module new file mode 100644 index 0000000000000000000000000000000000000000..dc6986dcdd218fb8605506892bc49a3aadeefe61 --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/plugin_test.module @@ -0,0 +1,6 @@ +<?php + +/** + * @file + * Helper module for the plugin tests. + */