summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDries2012-07-15 02:05:46 (GMT)
committerDries2012-07-15 02:05:46 (GMT)
commit5a5cdc702cb654a1f3e424ed349fcea0aeaa2fa7 (patch)
tree8e6492cc30de57ff8dc7649abe956a34c5c8327d
parent3cae919f52963d26486f73094f4a88525294edd1 (diff)
- Patch #1497366 by neclimdul, effulgentsia, EclipseGc, merlinofchaos: introduce Plugin System to core.
-rw-r--r--core/lib/Drupal/Component/Plugin/Derivative/DerivativeInterface.php44
-rw-r--r--core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscoveryDecorator.php157
-rw-r--r--core/lib/Drupal/Component/Plugin/Discovery/DiscoveryInterface.php35
-rw-r--r--core/lib/Drupal/Component/Plugin/Discovery/StaticDiscovery.php50
-rw-r--r--core/lib/Drupal/Component/Plugin/Exception/ExceptionInterface.php12
-rw-r--r--core/lib/Drupal/Component/Plugin/Exception/InvalidDecoratedMethod.php17
-rw-r--r--core/lib/Drupal/Component/Plugin/Exception/MapperExceptionInterface.php13
-rw-r--r--core/lib/Drupal/Component/Plugin/Exception/PluginException.php15
-rw-r--r--core/lib/Drupal/Component/Plugin/Factory/DefaultFactory.php71
-rw-r--r--core/lib/Drupal/Component/Plugin/Factory/FactoryInterface.php29
-rw-r--r--core/lib/Drupal/Component/Plugin/Factory/ReflectionFactory.php83
-rw-r--r--core/lib/Drupal/Component/Plugin/Mapper/MapperInterface.php33
-rw-r--r--core/lib/Drupal/Component/Plugin/PluginBase.php70
-rw-r--r--core/lib/Drupal/Component/Plugin/PluginInspectionInterface.php33
-rw-r--r--core/lib/Drupal/Component/Plugin/PluginManagerBase.php92
-rw-r--r--core/lib/Drupal/Component/Plugin/PluginManagerInterface.php31
-rw-r--r--core/lib/Drupal/Core/Plugin/Discovery/CacheDecorator.php124
-rw-r--r--core/lib/Drupal/Core/Plugin/Discovery/HookDiscovery.php57
-rw-r--r--core/modules/aggregator/aggregator.admin.inc16
-rw-r--r--core/modules/aggregator/aggregator.api.php57
-rw-r--r--core/modules/aggregator/aggregator.module33
-rw-r--r--core/modules/aggregator/lib/Drupal/aggregator/Plugin/FetcherInterface.php33
-rw-r--r--core/modules/aggregator/lib/Drupal/aggregator/Plugin/FetcherManager.php23
-rw-r--r--core/modules/aggregator/lib/Drupal/aggregator/Plugin/aggregator/fetcher/DefaultFetcher.php63
-rw-r--r--core/modules/simpletest/simpletest.info1
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Plugin/DerivativeTest.php42
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Plugin/DiscoveryTest.php38
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Plugin/FactoryTest.php86
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Plugin/InspectionTest.php41
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Plugin/PluginTestBase.php71
-rw-r--r--core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/MockBlockManager.php80
-rw-r--r--core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/TestPluginManager.php44
-rw-r--r--core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockLayoutBlock.php22
-rw-r--r--core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockLayoutBlockDeriver.php55
-rw-r--r--core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockMenuBlock.php54
-rw-r--r--core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockMenuBlockDeriver.php53
-rw-r--r--core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockUserLoginBlock.php35
-rw-r--r--core/modules/system/tests/modules/plugin_test/plugin_test.info6
-rw-r--r--core/modules/system/tests/modules/plugin_test/plugin_test.module6
39 files changed, 1766 insertions, 59 deletions
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 0000000..bb07069
--- /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 0000000..e245874
--- /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 0000000..dee79ea
--- /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 0000000..de74fb5
--- /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 0000000..57fb25b
--- /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 0000000..a3ef8e6
--- /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 0000000..222b547
--- /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 0000000..f83bdad
--- /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 0000000..39a260b
--- /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 0000000..7954745
--- /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 0000000..a81b868
--- /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 0000000..03610e6
--- /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 0000000..58c5a3b
--- /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 0000000..9157d7c
--- /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 0000000..73f1b55
--- /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 0000000..a7d74a8
--- /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 0000000..49a0d28
--- /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 0000000..3ff05f9
--- /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 7759750..0ee5ee6 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 0f708eb..3d7f499 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 0626f72..2d37b73 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
@@ -789,6 +797,19 @@ function _aggregator_items($count) {
}
/**
+ * 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.
*/
function aggregator_preprocess_block(&$variables) {
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 0000000..39fc19d
--- /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 0000000..ae24934
--- /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 0000000..e4b663b
--- /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 46f00c3..6e18c42 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 0000000..226b28d
--- /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 0000000..32637c6
--- /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 0000000..55c160f
--- /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 0000000..dcdaa34
--- /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 0000000..5595e28
--- /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 0000000..14484f5
--- /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 0000000..85fabeb
--- /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 0000000..4e8a2f5
--- /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 0000000..c654bd0
--- /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 0000000..84ec835
--- /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 0000000..aa84202
--- /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 0000000..e83e8cf
--- /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 0000000..406e02f
--- /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 0000000..dc6986d
--- /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.
+ */