From 5a5cdc702cb654a1f3e424ed349fcea0aeaa2fa7 Mon Sep 17 00:00:00 2001
From: Dries <dries@buytaert.net>
Date: Sat, 14 Jul 2012 22:05:46 -0400
Subject: [PATCH] - Patch #1497366 by neclimdul, effulgentsia, EclipseGc,
 merlinofchaos: introduce Plugin System to core.

---
 .../Plugin/Derivative/DerivativeInterface.php |  44 +++++
 .../DerivativeDiscoveryDecorator.php          | 157 ++++++++++++++++++
 .../Plugin/Discovery/DiscoveryInterface.php   |  35 ++++
 .../Plugin/Discovery/StaticDiscovery.php      |  50 ++++++
 .../Plugin/Exception/ExceptionInterface.php   |  12 ++
 .../Exception/InvalidDecoratedMethod.php      |  17 ++
 .../Exception/MapperExceptionInterface.php    |  13 ++
 .../Plugin/Exception/PluginException.php      |  15 ++
 .../Plugin/Factory/DefaultFactory.php         |  71 ++++++++
 .../Plugin/Factory/FactoryInterface.php       |  29 ++++
 .../Plugin/Factory/ReflectionFactory.php      |  83 +++++++++
 .../Plugin/Mapper/MapperInterface.php         |  33 ++++
 .../Drupal/Component/Plugin/PluginBase.php    |  70 ++++++++
 .../Plugin/PluginInspectionInterface.php      |  33 ++++
 .../Component/Plugin/PluginManagerBase.php    |  92 ++++++++++
 .../Plugin/PluginManagerInterface.php         |  31 ++++
 .../Core/Plugin/Discovery/CacheDecorator.php  | 124 ++++++++++++++
 .../Core/Plugin/Discovery/HookDiscovery.php   |  57 +++++++
 core/modules/aggregator/aggregator.admin.inc  |  16 +-
 core/modules/aggregator/aggregator.api.php    |  57 ++-----
 core/modules/aggregator/aggregator.module     |  33 +++-
 .../aggregator/Plugin/FetcherInterface.php    |  33 ++++
 .../aggregator/Plugin/FetcherManager.php      |  23 +++
 .../aggregator/fetcher/DefaultFetcher.php     |  63 +++++++
 core/modules/simpletest/simpletest.info       |   1 +
 .../system/Tests/Plugin/DerivativeTest.php    |  42 +++++
 .../system/Tests/Plugin/DiscoveryTest.php     |  38 +++++
 .../system/Tests/Plugin/FactoryTest.php       |  86 ++++++++++
 .../system/Tests/Plugin/InspectionTest.php    |  41 +++++
 .../system/Tests/Plugin/PluginTestBase.php    |  71 ++++++++
 .../plugin_test/Plugin/MockBlockManager.php   |  80 +++++++++
 .../plugin_test/Plugin/TestPluginManager.php  |  44 +++++
 .../mock_block/MockLayoutBlock.php            |  22 +++
 .../mock_block/MockLayoutBlockDeriver.php     |  55 ++++++
 .../plugin_test/mock_block/MockMenuBlock.php  |  54 ++++++
 .../mock_block/MockMenuBlockDeriver.php       |  53 ++++++
 .../mock_block/MockUserLoginBlock.php         |  35 ++++
 .../modules/plugin_test/plugin_test.info      |   6 +
 .../modules/plugin_test/plugin_test.module    |   6 +
 39 files changed, 1766 insertions(+), 59 deletions(-)
 create mode 100644 core/lib/Drupal/Component/Plugin/Derivative/DerivativeInterface.php
 create mode 100644 core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscoveryDecorator.php
 create mode 100644 core/lib/Drupal/Component/Plugin/Discovery/DiscoveryInterface.php
 create mode 100644 core/lib/Drupal/Component/Plugin/Discovery/StaticDiscovery.php
 create mode 100644 core/lib/Drupal/Component/Plugin/Exception/ExceptionInterface.php
 create mode 100644 core/lib/Drupal/Component/Plugin/Exception/InvalidDecoratedMethod.php
 create mode 100644 core/lib/Drupal/Component/Plugin/Exception/MapperExceptionInterface.php
 create mode 100644 core/lib/Drupal/Component/Plugin/Exception/PluginException.php
 create mode 100644 core/lib/Drupal/Component/Plugin/Factory/DefaultFactory.php
 create mode 100644 core/lib/Drupal/Component/Plugin/Factory/FactoryInterface.php
 create mode 100644 core/lib/Drupal/Component/Plugin/Factory/ReflectionFactory.php
 create mode 100644 core/lib/Drupal/Component/Plugin/Mapper/MapperInterface.php
 create mode 100644 core/lib/Drupal/Component/Plugin/PluginBase.php
 create mode 100644 core/lib/Drupal/Component/Plugin/PluginInspectionInterface.php
 create mode 100644 core/lib/Drupal/Component/Plugin/PluginManagerBase.php
 create mode 100644 core/lib/Drupal/Component/Plugin/PluginManagerInterface.php
 create mode 100644 core/lib/Drupal/Core/Plugin/Discovery/CacheDecorator.php
 create mode 100644 core/lib/Drupal/Core/Plugin/Discovery/HookDiscovery.php
 create mode 100644 core/modules/aggregator/lib/Drupal/aggregator/Plugin/FetcherInterface.php
 create mode 100644 core/modules/aggregator/lib/Drupal/aggregator/Plugin/FetcherManager.php
 create mode 100644 core/modules/aggregator/lib/Drupal/aggregator/Plugin/aggregator/fetcher/DefaultFetcher.php
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Plugin/DerivativeTest.php
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Plugin/DiscoveryTest.php
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Plugin/FactoryTest.php
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Plugin/InspectionTest.php
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Plugin/PluginTestBase.php
 create mode 100644 core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/MockBlockManager.php
 create mode 100644 core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/TestPluginManager.php
 create mode 100644 core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockLayoutBlock.php
 create mode 100644 core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockLayoutBlockDeriver.php
 create mode 100644 core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockMenuBlock.php
 create mode 100644 core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockMenuBlockDeriver.php
 create mode 100644 core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/plugin_test/mock_block/MockUserLoginBlock.php
 create mode 100644 core/modules/system/tests/modules/plugin_test/plugin_test.info
 create mode 100644 core/modules/system/tests/modules/plugin_test/plugin_test.module

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 000000000000..bb0706907582
--- /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 000000000000..e245874d4c5c
--- /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 000000000000..dee79eae09ec
--- /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 000000000000..de74fb5a902e
--- /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 000000000000..57fb25bfc528
--- /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 000000000000..a3ef8e636c95
--- /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 000000000000..222b5473d0aa
--- /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 000000000000..f83bdad3d29c
--- /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 000000000000..39a260b6b8c9
--- /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 000000000000..7954745a1cb3
--- /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 000000000000..a81b868d6fa9
--- /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 000000000000..03610e6490cc
--- /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 000000000000..58c5a3bd2f07
--- /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 000000000000..9157d7c43179
--- /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 000000000000..73f1b5542dac
--- /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 000000000000..a7d74a8092e1
--- /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 000000000000..49a0d2863d9f
--- /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 000000000000..3ff05f9197ff
--- /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 7759750ba3a9..0ee5ee61f197 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 0f708eb85ef3..3d7f4996fd05 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 0626f726e3c8..2d37b73de9bd 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 000000000000..39fc19df2aee
--- /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 000000000000..ae24934d1934
--- /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 000000000000..e4b663ba47bb
--- /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 46f00c3a32bb..6e18c42bd3af 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 000000000000..226b28d6f290
--- /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 000000000000..32637c672887
--- /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 000000000000..55c160f974ef
--- /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 000000000000..dcdaa34f30bc
--- /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 000000000000..5595e28bce5d
--- /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 000000000000..14484f5c69c3
--- /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 000000000000..85fabeb4698f
--- /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 000000000000..4e8a2f562060
--- /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 000000000000..c654bd04b8d0
--- /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 000000000000..84ec835bfcde
--- /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 000000000000..aa84202bbfc2
--- /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 000000000000..e83e8cf52435
--- /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 000000000000..406e02fd9844
--- /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 000000000000..dc6986dcdd21
--- /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.
+ */
-- 
GitLab