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 @@ +decorated = $decorated; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getPluginDefinition(). + */ + public function getPluginDefinition($plugin_id) { + + list($base_plugin_id, $derivative_id) = $this->decodePluginId($plugin_id); + + $plugin_definition = $this->decorated->getPluginDefinition($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::getPluginDefinitions(). + */ + public function getPluginDefinitions() { + $plugin_definitions = $this->decorated->getPluginDefinitions(); + return $this->getDerivatives($plugin_definitions); + } + + /** + * Adds derivatives to a list of plugin definitions. + * + * This should be called by the class extending this in + * DiscoveryInterface::getPluginDefinitions(). + */ + 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..de4afdb --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryInterface.php @@ -0,0 +1,35 @@ +pluginDefinitions[$base_plugin_id]) ? $this->pluginDefinitions[$base_plugin_id] : NULL; + } + + /** + * Implements DerivativeAwareDiscovery::getBasePluginDefinitions(). + */ + public function getPluginDefinitions() { + return $this->pluginDefinitions; + } + + /** + * Sets a plugin definition. + */ + public function setPluginDefinition($plugin, array $definition) { + $this->pluginDefinitions[$plugin] = $definition; + } + + /** + * Deletes a plugin definition. + */ + public function deletePluginDefinition($plugin) { + unset($this->pluginDefinitions[$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 @@ +discovery = $discovery; + $this->classKey = $class_key; + } + + /** + * Implements FactoryInterface::createPluginInstance(). + */ + public function createPluginInstance($plugin_id, array $configuration) { + $plugin_class = $this->getPluginClass($plugin_id); + return new $plugin_class($configuration); + } + + /** + * 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) { + if (empty($this->classKey)) { + throw new PluginException("The plugin type did not specify a valid key for determining the plugin instance class."); + } + + $plugin_definition = $this->discovery->getPluginDefinition($plugin_id); + if (empty($plugin_definition[$this->classKey])) { + throw new PluginException("The plugin did not specify an instance class."); + } + + $class = $plugin_definition[$this->classKey]; + + if (!class_exists($class)) { + throw new PluginException(t("Plugin instance class @class does not exist.", array('@class' => $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..3a15583 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Factory/FactoryInterface.php @@ -0,0 +1,29 @@ +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->getPluginInstanceArguments($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 getPluginInstanceArguments(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 { + // Missing constructor argument. + // TODO - Do we throw an exception or just skip and let sub classes + // extend this. + } + } + return $arguments; + } +} diff --git a/core/lib/Drupal/Component/Plugin/Mapper/DefaultMapper.php b/core/lib/Drupal/Component/Plugin/Mapper/DefaultMapper.php new file mode 100644 index 0000000..ada30ec --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Mapper/DefaultMapper.php @@ -0,0 +1,92 @@ +discovery = $discovery; + $this->factory = $factory; + $this->pluginIdKey = isset($options['id_key']) ? $options['id_key'] : 'plugin'; + $this->defaultPluginId = isset($options['default']) ? $options['default'] : 'default'; + } + + /** + * Implements Drupal\Component\Plugin\Mapper\MapperInterface::getPluginInstance(). + * + * This implements a simple mapper that does the following: + * - Maps a single $options key to the desired plugin id. + * - If that plugin doesn't exist, uses a default one. + * - Forwards all of $options to the plugin instance configuration. + */ + public function getPluginInstance(array $options) { + $instance_id = $this->getInstanceId($options); + if (!isset($this->instances[$instance_id])) { + $plugin_id = ($this->discovery->getPluginDefinition($options[$this->pluginIdKey]) !== NULL) ? $options[$this->pluginIdKey] : $this->defaultPluginId; + $configuration = $options; + $this->instances[$instance_id] = $this->factory->createPluginInstance($plugin_id, $configuration); + } + return $this->instances[$instance_id]; + } + + /** + * Returns an identifier for a given set of plugin options in order to reuse instances. + * + * @param array $options + * @todo + * + * @return + * @todo + */ + protected function getInstanceId(array $options) { + return serialize($options); + } +} 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..ab20693 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Mapper/MapperInterface.php @@ -0,0 +1,33 @@ +plugin_id = $plugin_id; + $this->discovery = $discovery; + $this->configuration = $configuration; + } + + /** + * Implements Drupal\Component\Plugin\PluginInspectionInterface::getPluginId(). + */ + public function getPluginId() { + return $this->plugin_id; + } + + /** + * Implements Drupal\Component\Plugin\PluginInspectionInterface::getPluginDefinition(). + */ + public function getpluginDefinition() { + return $this->discovery->getPluginDefinition($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..e49898d --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/PluginInspectionInterface.php @@ -0,0 +1,33 @@ +processPluginDefinition() if + * additional processing of plugins is necessary or helpful for development + * purposes. + * + * @var array + */ + protected $defaults = array(); + + /** + * Implements Drupal\Component\Plugin\PluginTypeInterface::getPluginDefinition(). + */ + public function getPluginDefinition($plugin_id) { + $definition = $this->discovery->getPluginDefinition($plugin_id); + if (isset($definition)) { + $this->processPluginDefinition($definition, $plugin_id); + } + return $definition; + } + + /** + * Implements Drupal\Component\Plugin\PluginTypeInterface::getPluginDefinitions(). + */ + public function getPluginDefinitions() { + $definitions = $this->discovery->getPluginDefinitions(); + foreach ($definitions as $plugin_id => &$definition) { + $this->processPluginDefinition($definition, $plugin_id); + } + + return $definitions; + } + + /** + * Implements Drupal\Component\Plugin\PluginTypeInterface::createPluginInstance(). + */ + public function createPluginInstance($plugin_id, array $configuration = array()) { + return $this->factory->createPluginInstance($plugin_id, $configuration); + } + + /** + * Implements Drupal\Component\Plugin\PluginTypeInterface::getPluginInstance(). + */ + public function getPluginInstance(array $options) { + return $this->mapper->getPluginInstance($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 processPluginDefinition(&$definition, $plugin_id) { + $definition += $this->defaults; + } +} diff --git a/core/lib/Drupal/Component/Plugin/PluginTypeInterface.php b/core/lib/Drupal/Component/Plugin/PluginTypeInterface.php new file mode 100644 index 0000000..7487a66 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/PluginTypeInterface.php @@ -0,0 +1,24 @@ +decorated = $decorated; + $this->cacheKey = $cache_key; + $this->cacheBin = $cache_bin; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DicoveryInterface::getPluginDefinition(). + */ + public function getPluginDefinition($plugin_id) { + $definitions = $this->getCachedDefinitions(); + if (isset($definitions)) { + $definition = isset($definitions[$plugin_id]) ? $definitions[$plugin_id] : NULL; + } + else { + $definition = $this->decorated->getPluginDefinition($plugin_id); + } + return $definition; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DicoveryInterface::getPluginDefinitions(). + */ + public function getPluginDefinitions() { + $definitions = $this->getCachedDefinitions(); + if (!isset($definitions)) { + $definitions = $this->decorated->getPluginDefinitions(); + $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 getPluginDefinitions() 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..6aba72c --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Discovery/HookDiscovery.php @@ -0,0 +1,57 @@ +hook = $hook; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DicoveryInterface::getPluginDefinition(). + */ + public function getPluginDefinition($plugin_id) { + $plugins = $this->getPluginDefinitions(); + return isset($plugins[$plugin_id]) ? $plugins[$plugin_id] : array(); + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DicoveryInterface::getPluginDefinitions(). + */ + public function getPluginDefinitions() { + 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/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/DerivativePluginTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/DerivativePluginTest.php new file mode 100644 index 0000000..0c15368 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/DerivativePluginTest.php @@ -0,0 +1,80 @@ + 'Derivative Discovery', + 'description' => 'Tests the the plugin system derivative integration.', + 'group' => 'Plugin API', + ); + } + + public function setUp() { + parent::setUp(); + + // Real modules implementing plugin types may expose a module-specific API + // for retrieving its plugin type objects, or make them available in + // Drupal's dependency injection container, but for unit testing, we get + // the object directly. + $this->testPluginType = new MockBlockPluginType(); + } + + /** + * Tests getPluginDefinitions() and getPluginDefinition() with a derivativeDecorator. + */ + function testDerivativeDecorator() { + + // @see MockBlockPluginType::_construct(). + $expected = array( + 'user_login' => array( + 'label' => 'User login', + 'class' => 'Drupal\plugin_test\Plugin\MockUserLoginBlock', + ), + 'menu:main_menu' => array( + 'label' => 'Main menu', + 'class' => 'Drupal\plugin_test\Plugin\MockMenuBlock', + ), + 'menu:navigation' => array( + 'label' => 'Navigation', + 'class' => 'Drupal\plugin_test\Plugin\MockMenuBlock', + ), + 'layout' => array( + 'label' => 'Layout', + 'class' => 'Drupal\plugin_test\Plugin\MockLayoutBlock', + ), + 'layout:foo' => array( + 'label' => 'Layout Foo', + 'class' => 'Drupal\plugin_test\Plugin\MockLayoutBlock', + ), + ); + + // Ensure that getPluginDefinitions() returns the expected definitions. + $this->assertIdentical($this->testPluginType->getPluginDefinitions(), $expected); + + // Ensure that getPluginDefinition() returns the expected definition. + foreach ($expected as $id => $definition) { + $this->assertIdentical($this->testPluginType->getPluginDefinition($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->testPluginType->getPluginDefinition('non_existing'), NULL, 'NULL returned as the definition of a non-existing base plugin.'); + $this->assertIdentical($this->testPluginType->getPluginDefinition('menu:non_existing'), NULL, 'NULL returned as the definition of a non-existing derivative plugin.'); + $this->assertIdentical($this->testPluginType->getPluginDefinition('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/DiscoveryPluginTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/DiscoveryPluginTest.php new file mode 100644 index 0000000..967946c --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/DiscoveryPluginTest.php @@ -0,0 +1,62 @@ + 'Discovery API', + 'description' => 'Tests the the plugin system API.', + 'group' => 'Plugin API', + ); + } + + public function setUp() { + parent::setUp(); + + // Real modules implementing plugin types may expose a module-specific API + // for retrieving its plugin type objects, or make them available in + // Drupal's dependency injection container, but for unit testing, we get + // the object directly. + + // A very basic mock plugin type. + $this->testPluginType = new TestPluginType(); + } + + /** + * Tests getPluginDefinitions() and getPluginDefinition(). + */ + function testDiscoveryInterface() { + + // @see TestPluginType::_construct(). + $expected = array( + 'user_login' => array( + 'label' => 'User login', + 'class' => 'Drupal\plugin_test\Plugin\MockUserLoginBlock', + ), + ); + + // Ensure that getPluginDefinitions() returns the expected definitions. + $this->assertIdentical($this->testPluginType->getPluginDefinitions(), $expected); + + // Ensure that getPluginDefinition() returns the expected definition. + foreach ($expected as $id => $definition) { + $this->assertIdentical($this->testPluginType->getPluginDefinition($id), $definition); + } + + // Ensure that NULL is returned as the definition of a non-existing plugin. + $this->assertIdentical($this->testPluginType->getPluginDefinition('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/FactoryPluginTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/FactoryPluginTest.php new file mode 100644 index 0000000..21f366b --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/FactoryPluginTest.php @@ -0,0 +1,105 @@ + 'Factory Tests', + 'description' => 'Test the base plugin factories.', + 'group' => 'Plugin API', + ); + } + + public function setUp() { + parent::setUp(); + + // Real modules implementing plugin types may expose a module-specific API + // for retrieving its plugin type objects, or make them available in + // Drupal's dependency injection container, but for unit testing, we get + // the object directly. + + // A very basic mock plugin type. + $this->testPluginType = new TestPluginType(); + // A slightly more advanced plugin type mocking a block system with + // derivatives. + $this->mockBlockPluginType = new MockBlockPluginType(); + } + + /** + * Test that DefaultFactory can create a plugin instance. + */ + function testDefaultFactory() { + // Ensure a non-derivative plugin can be instantiated. + $plugin = $this->testPluginType->createPluginInstance('user_login', array('title' => 'Please enter your login name and password')); + $this->assertIdentical(get_class($plugin), 'Drupal\plugin_test\Plugin\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->testPluginType->createPluginInstance('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->mockBlockPluginType->createPluginInstance('user_login', array('title' => 'Please enter your login name and password')); + $this->assertIdentical(get_class($plugin), 'Drupal\plugin_test\Plugin\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->mockBlockPluginType->createPluginInstance('menu:main_menu', array('depth' => 2)); + $this->assertIdentical($plugin->getContent(), '