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(), '', '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->mockBlockPluginType->createPluginInstance($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/PluginTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/PluginTestBase.php new file mode 100644 index 0000000..3071471 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/PluginTestBase.php @@ -0,0 +1,33 @@ +getDerivativeDefinitions($base_plugin_definition); + if (isset($derivatives[$derivative_id])) { + return $derivatives[$derivative_id]; + } + } + + /** + * Implements 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/MockMenuBlock.php b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/MockMenuBlock.php new file mode 100644 index 0000000..b414006 --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/MockMenuBlock.php @@ -0,0 +1,54 @@ +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: + // ''. + $content = ''; + for ($i=0; $i < $this->depth; $i++) { + $content .= ''; + } + return $content; + } +} diff --git a/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/MockMenuBlockDeriver.php b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/MockMenuBlockDeriver.php new file mode 100644 index 0000000..b2f44c8 --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/MockMenuBlockDeriver.php @@ -0,0 +1,53 @@ +getDerivativeDefinitions($base_plugin_definition); + if (isset($derivatives[$derivative_id])) { + return $derivatives[$derivative_id]; + } + } + + /** + * Implements 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/MockUserLoginBlock.php b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/MockUserLoginBlock.php new file mode 100644 index 0000000..b61ba2a --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/MockUserLoginBlock.php @@ -0,0 +1,33 @@ +title = $configuration['title']; + } + + public function getTitle() { + return $this->title; + } +} diff --git a/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/Type/MockBlockPluginType.php b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/Type/MockBlockPluginType.php new file mode 100644 index 0000000..351f85c --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/Type/MockBlockPluginType.php @@ -0,0 +1,80 @@ +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 types 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->setPluginDefinition('user_login', array( + 'label' => 'User login', + 'class' => 'Drupal\plugin_test\Plugin\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->setPluginDefinition('menu', array( + 'class' => 'Drupal\plugin_test\Plugin\MockMenuBlock', + 'derivative' => 'Drupal\plugin_test\Plugin\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->setPluginDefinition('layout', array( + 'label' => 'Layout', + 'class' => 'Drupal\plugin_test\Plugin\MockLayoutBlock', + 'derivative' => 'Drupal\plugin_test\Plugin\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 types 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/Type/TestPluginType.php b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/Type/TestPluginType.php new file mode 100644 index 0000000..d877e45 --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/Plugin/Type/TestPluginType.php @@ -0,0 +1,44 @@ +discovery = new StaticDiscovery(); + + // A simple plugin: a mock user login block. + $this->discovery->setPluginDefinition('user_login', array( + 'label' => 'User login', + 'class' => 'Drupal\plugin_test\Plugin\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 types 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/plugin_test.info b/core/modules/system/tests/modules/plugin_test/plugin_test.info new file mode 100644 index 0000000..406e02f --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/plugin_test.info @@ -0,0 +1,6 @@ +name = "Plugin Test Support" +description = "Test that plugins can provide plugins and provide namespace discovery for plugin test implementations." +package = Testing +version = VERSION +core = 8.x +hidden = TRUE diff --git a/core/modules/system/tests/modules/plugin_test/plugin_test.module b/core/modules/system/tests/modules/plugin_test/plugin_test.module new file mode 100644 index 0000000..dc6986d --- /dev/null +++ b/core/modules/system/tests/modules/plugin_test/plugin_test.module @@ -0,0 +1,6 @@ +