From 47e74965af015565d1508d67ff73e4065e6fae45 Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Tue, 10 Jun 2014 21:09:10 +0100 Subject: [PATCH] Issue #2277981 by tim.plunkett, EclipseGc, fago: Provide a service for handling context-aware plugins. --- core/core.services.yml | 3 + .../Core/Condition/ConditionManager.php | 3 + .../ContextAwarePluginManagerInterface.php | 30 ++ .../ContextAwarePluginManagerTrait.php | 36 ++ .../Core/Plugin/Context/ContextHandler.php | 144 +++++++ .../Context/ContextHandlerInterface.php | 83 ++++ core/modules/block/src/BlockManager.php | 4 +- .../block/src/BlockManagerInterface.php | 4 +- core/modules/block/src/Tests/BlockUiTest.php | 18 + .../Plugin/Block/TestContextAwareBlock.php | 38 ++ .../Tests/Core/Plugin/ContextHandlerTest.php | 403 ++++++++++++++++++ 11 files changed, 763 insertions(+), 3 deletions(-) create mode 100644 core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerInterface.php create mode 100644 core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerTrait.php create mode 100644 core/lib/Drupal/Core/Plugin/Context/ContextHandler.php create mode 100644 core/lib/Drupal/Core/Plugin/Context/ContextHandlerInterface.php create mode 100644 core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php create mode 100644 core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php diff --git a/core/core.services.yml b/core/core.services.yml index d0facb4c4e30..9466f9521864 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -115,6 +115,9 @@ services: config.typed: class: Drupal\Core\Config\TypedConfigManager arguments: ['@config.storage', '@config.storage.schema', '@cache.discovery'] + context.handler: + class: Drupal\Core\Plugin\Context\ContextHandler + arguments: ['@typed_data_manager'] cron: class: Drupal\Core\Cron arguments: ['@module_handler', '@lock', '@queue', '@state', '@current_user', '@session_manager'] diff --git a/core/lib/Drupal/Core/Condition/ConditionManager.php b/core/lib/Drupal/Core/Condition/ConditionManager.php index 4b4e6599d6d0..ca24adddbbb7 100644 --- a/core/lib/Drupal/Core/Condition/ConditionManager.php +++ b/core/lib/Drupal/Core/Condition/ConditionManager.php @@ -12,6 +12,7 @@ use Drupal\Core\Executable\ExecutableInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageManager; +use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait; use Drupal\Core\Plugin\DefaultPluginManager; /** @@ -19,6 +20,8 @@ */ class ConditionManager extends DefaultPluginManager implements ExecutableManagerInterface { + use ContextAwarePluginManagerTrait; + /** * Constructs a ConditionManager object. * diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerInterface.php b/core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerInterface.php new file mode 100644 index 000000000000..7bf137dc81af --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerInterface.php @@ -0,0 +1,30 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Plugin\Context\ContextAwarePluginManagerInterface. + */ + +namespace Drupal\Core\Plugin\Context; + +use Drupal\Component\Plugin\PluginManagerInterface; + +/** + * Provides an interface for plugin managers that support context-aware plugins. + */ +interface ContextAwarePluginManagerInterface extends PluginManagerInterface { + + /** + * Determines plugins whose constraints are satisfied by a set of contexts. + * + * @todo Use context definition objects after https://drupal.org/node/2281635. + * + * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts + * An array of contexts. + * + * @return array + * An array of plugin definitions. + */ + public function getDefinitionsForContexts(array $contexts = array()); + +} diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerTrait.php b/core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerTrait.php new file mode 100644 index 000000000000..407b789cc303 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerTrait.php @@ -0,0 +1,36 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait. + */ + +namespace Drupal\Core\Plugin\Context; + +/** + * Provides a trait for plugin managers that support context-aware plugins. + */ +trait ContextAwarePluginManagerTrait { + + /** + * Wraps the context handler. + * + * @return \Drupal\Core\Plugin\Context\ContextHandlerInterface + */ + protected function contextHandler() { + return \Drupal::service('context.handler'); + } + + /** + * See \Drupal\Core\Plugin\Context\ContextAwarePluginManagerInterface::getDefinitionsForContexts(). + */ + public function getDefinitionsForContexts(array $contexts = array()) { + return $this->contextHandler()->filterPluginDefinitionsByContexts($contexts, $this->getDefinitions()); + } + + /** + * See \Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinitions(). + */ + abstract public function getDefinitions(); + +} diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php new file mode 100644 index 000000000000..68fbda3eae64 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php @@ -0,0 +1,144 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Plugin\Context\ContextHandler. + */ + +namespace Drupal\Core\Plugin\Context; + +use Drupal\Component\Plugin\Context\ContextInterface; +use Drupal\Component\Plugin\ContextAwarePluginInterface; +use Drupal\Component\Plugin\Exception\ContextException; +use Drupal\Component\Utility\String; +use Drupal\Core\TypedData\DataDefinitionInterface; +use Drupal\Core\TypedData\DataDefinition; +use Drupal\Core\TypedData\TypedDataManager; + +/** + * Provides methods to handle sets of contexts. + */ +class ContextHandler implements ContextHandlerInterface { + + /** + * The typed data manager. + * + * @var \Drupal\Core\TypedData\TypedDataManager + */ + protected $typedDataManager; + + /** + * Constructs a new ContextHandler. + * + * @param \Drupal\Core\TypedData\TypedDataManager $typed_data + * The typed data manager. + */ + public function __construct(TypedDataManager $typed_data) { + $this->typedDataManager = $typed_data; + } + + /** + * {@inheritdoc} + */ + public function filterPluginDefinitionsByContexts(array $contexts, array $definitions) { + return array_filter($definitions, function ($plugin_definition) use ($contexts) { + // If this plugin doesn't need any context, it is available to use. + if (!isset($plugin_definition['context'])) { + return TRUE; + } + + // Build an array of requirements out of the contexts specified by the + // plugin definition. + $requirements = array(); + foreach ($plugin_definition['context'] as $context_id => $plugin_context) { + $definition = $this->typedDataManager->getDefinition($plugin_context['type']); + $definition['type'] = $plugin_context['type']; + + // If the plugin specifies additional constraints, add them to the + // constraints defined by the plugin type. + if (isset($plugin_context['constraints'])) { + // Ensure the array exists before adding in constraints. + if (!isset($definition['constraints'])) { + $definition['constraints'] = array(); + } + + $definition['constraints'] += $plugin_context['constraints']; + } + + // Assume the requirement is required if unspecified. + if (!isset($definition['required'])) { + $definition['required'] = TRUE; + } + + // @todo Use context definition objects after + // https://drupal.org/node/2281635. + $requirements[$context_id] = new DataDefinition($definition); + } + + // Check the set of contexts against the requirements. + return $this->checkRequirements($contexts, $requirements); + }); + } + + /** + * {@inheritdoc} + */ + public function checkRequirements(array $contexts, array $requirements) { + foreach ($requirements as $requirement) { + if ($requirement->isRequired() && !$this->getMatchingContexts($contexts, $requirement)) { + return FALSE; + } + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getMatchingContexts(array $contexts, DataDefinitionInterface $definition) { + return array_filter($contexts, function (ContextInterface $context) use ($definition) { + // @todo getContextDefinition() should return a DataDefinitionInterface. + $context_definition = new DataDefinition($context->getContextDefinition()); + + // If the data types do not match, this context is invalid. + if ($definition->getDataType() != $context_definition->getDataType()) { + return FALSE; + } + + // If any constraint does not match, this context is invalid. + // @todo This is too restrictive, consider only relying on data types. + foreach ($definition->getConstraints() as $constraint_name => $constraint) { + if ($context_definition->getConstraint($constraint_name) != $constraint) { + return FALSE; + } + } + + // All contexts with matching data type and contexts are valid. + return TRUE; + }); + } + + /** + * {@inheritdoc} + */ + public function applyContextMapping(ContextAwarePluginInterface $plugin, $contexts, $mappings = array()) { + $plugin_contexts = $plugin->getContextDefinitions(); + // Loop through each context and set it on the plugin if it matches one of + // the contexts expected by the plugin. + foreach ($contexts as $name => $context) { + // If this context was given a specific name, use that. + $assigned_name = isset($mappings[$name]) ? $mappings[$name] : $name; + if (isset($plugin_contexts[$assigned_name])) { + // This assignment has been used, remove it. + unset($mappings[$name]); + $plugin->setContextValue($assigned_name, $context->getContextValue()); + } + } + + // If there are any mappings that were not satisfied, throw an exception. + if (!empty($mappings)) { + throw new ContextException(String::format('Assigned contexts were not satisfied: @mappings', array('@mappings' => implode(',', $mappings)))); + } + } + +} diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextHandlerInterface.php b/core/lib/Drupal/Core/Plugin/Context/ContextHandlerInterface.php new file mode 100644 index 000000000000..b39df2ced0ec --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandlerInterface.php @@ -0,0 +1,83 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Plugin\Context\ContextHandlerInterface. + */ + +namespace Drupal\Core\Plugin\Context; + +use Drupal\Component\Plugin\ContextAwarePluginInterface; +use Drupal\Core\TypedData\DataDefinitionInterface; + +/** + * Provides an interface for handling sets of contexts. + */ +interface ContextHandlerInterface { + + /** + * Determines plugins whose constraints are satisfied by a set of contexts. + * + * @todo Use context definition objects after https://drupal.org/node/2281635. + * + * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts + * An array of contexts. + * @param array $definitions . + * An array of plugin definitions. + * + * @return array + * An array of plugin definitions. + */ + public function filterPluginDefinitionsByContexts(array $contexts, array $definitions); + + /** + * Checks a set of requirements against a set of contexts. + * + * @todo Use context definition objects after https://drupal.org/node/2281635. + * + * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts + * An array of available contexts. + * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $requirements + * An array of requirements. + * + * @return bool + * TRUE if all of the requirements are satisfied by the context, FALSE + * otherwise. + */ + public function checkRequirements(array $contexts, array $requirements); + + /** + * Determines which contexts satisfy the constraints of a given definition. + * + * @todo Use context definition objects after https://drupal.org/node/2281635. + * + * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts + * An array of contexts. + * @param \Drupal\Core\TypedData\DataDefinitionInterface $definition + * The definition to satisfy. + * + * @return \Drupal\Component\Plugin\Context\ContextInterface[] + * An array of matching contexts. + */ + public function getMatchingContexts(array $contexts, DataDefinitionInterface $definition); + + /** + * Prepares a plugin for evaluation. + * + * @param \Drupal\Component\Plugin\ContextAwarePluginInterface $plugin + * A plugin about to be evaluated. + * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts + * An array of contexts to set on the plugin. They will only be set if they + * match the plugin's context definitions. + * @param array $mappings + * (optional) A mapping of the expected assignment names to their context + * names. For example, if one of the $contexts is named 'entity', but the + * plugin expects a context named 'node', then this map would contain + * 'entity' => 'node'. + * + * @throws \Drupal\Component\Plugin\Exception\ContextException + * Thrown when a context assignment was not satisfied. + */ + public function applyContextMapping(ContextAwarePluginInterface $plugin, $contexts, $mappings = array()); + +} diff --git a/core/modules/block/src/BlockManager.php b/core/modules/block/src/BlockManager.php index 4a1651b432c0..1db10a6f9b56 100644 --- a/core/modules/block/src/BlockManager.php +++ b/core/modules/block/src/BlockManager.php @@ -10,6 +10,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageManager; +use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait; use Drupal\Core\Plugin\DefaultPluginManager; use Drupal\Core\StringTranslation\TranslationInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -24,6 +25,7 @@ class BlockManager extends DefaultPluginManager implements BlockManagerInterface { use StringTranslationTrait; + use ContextAwarePluginManagerTrait; /** * An array of all available modules and their data. @@ -106,7 +108,7 @@ public function getCategories() { */ public function getSortedDefinitions() { // Sort the plugins first by category, then by label. - $definitions = $this->getDefinitions(); + $definitions = $this->getDefinitionsForContexts(); uasort($definitions, function ($a, $b) { if ($a['category'] != $b['category']) { return strnatcasecmp($a['category'], $b['category']); diff --git a/core/modules/block/src/BlockManagerInterface.php b/core/modules/block/src/BlockManagerInterface.php index 4973f166ec84..665600499b3d 100644 --- a/core/modules/block/src/BlockManagerInterface.php +++ b/core/modules/block/src/BlockManagerInterface.php @@ -7,12 +7,12 @@ namespace Drupal\block; -use Drupal\Component\Plugin\PluginManagerInterface; +use Drupal\Core\Plugin\Context\ContextAwarePluginManagerInterface; /** * Provides an interface for the discovery and instantiation of block plugins. */ -interface BlockManagerInterface extends PluginManagerInterface { +interface BlockManagerInterface extends ContextAwarePluginManagerInterface { /** * Gets the names of all block categories. diff --git a/core/modules/block/src/Tests/BlockUiTest.php b/core/modules/block/src/Tests/BlockUiTest.php index d842401a1634..5c2dbfa88881 100644 --- a/core/modules/block/src/Tests/BlockUiTest.php +++ b/core/modules/block/src/Tests/BlockUiTest.php @@ -157,6 +157,24 @@ public function testCandidateBlockList() { $this->assertTrue(!empty($elements), 'The test block appears in a custom category controlled by block_test_block_alter().'); } + /** + * Tests the behavior of context-aware blocks. + */ + public function testContextAwareBlocks() { + $arguments = array( + ':ul_class' => 'block-list', + ':li_class' => 'test-context-aware', + ':href' => 'admin/structure/block/add/test_context_aware/stark', + ':text' => 'Test context-aware block', + ); + + $this->drupalGet('admin/structure/block'); + $elements = $this->xpath('//details[@id="edit-category-block-test"]//ul[contains(@class, :ul_class)]/li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments); + $this->assertTrue(empty($elements), 'The context-aware test block does not appear.'); + $definition = \Drupal::service('plugin.manager.block')->getDefinition('test_context_aware'); + $this->assertTrue(!empty($definition), 'The context-aware test block exists.'); + } + /** * Tests that the BlockForm populates machine name correctly. */ diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php new file mode 100644 index 000000000000..ae2550b4bcc3 --- /dev/null +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php @@ -0,0 +1,38 @@ +<?php + +/** + * @file + * Contains \Drupal\block_test\Plugin\Block\TestContextAwareBlock. + */ + +namespace Drupal\block_test\Plugin\Block; + +use Drupal\block\BlockBase; + +/** + * Provides a context-aware block. + * + * @Block( + * id = "test_context_aware", + * admin_label = @Translation("Test context-aware block"), + * context = { + * "user" = { + * "type" = "entity:user" + * } + * } + * ) + */ +class TestContextAwareBlock extends BlockBase { + + /** + * {@inheritdoc} + */ + public function build() { + /** @var $user \Drupal\user\UserInterface */ + $user = $this->getContextValue('user'); + return array( + '#markup' => $user->getUsername(), + ); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php b/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php new file mode 100644 index 000000000000..d162042d1519 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php @@ -0,0 +1,403 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Plugin\ContextHandlerTest. + */ + +namespace Drupal\Tests\Core\Plugin; + +use Drupal\Component\Plugin\ConfigurablePluginInterface; +use Drupal\Component\Plugin\ContextAwarePluginInterface; +use Drupal\Core\Plugin\Context\ContextHandler; +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Tests\UnitTestCase; + +/** + * Tests the ContextHandler class. + * + * @coversDefaultClass \Drupal\Core\Plugin\ContextHandler + * + * @group Drupal + * @group Plugin + * @group Context + */ +class ContextHandlerTest extends UnitTestCase { + + /** + * The typed data manager. + * + * @var \Drupal\Core\TypedData\TypedDataManager|\PHPUnit_Framework_MockObject_MockObject + */ + protected $typedDataManager; + + /** + * The context handler. + * + * @var \Drupal\Core\Plugin\Context\ContextHandler + */ + protected $contextHandler; + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'ContextHandler', + 'description' => 'Tests the ContextHandler', + 'group' => 'Plugin API', + ); + } + + /** + * {@inheritdoc} + * + * @covers ::__construct + */ + protected function setUp() { + parent::setUp(); + + $this->typedDataManager = $this->getMockBuilder('Drupal\Core\TypedData\TypedDataManager') + ->disableOriginalConstructor() + ->getMock(); + $this->typedDataManager->expects($this->any()) + ->method('getDefaultConstraints') + ->will($this->returnValue(array())); + $this->contextHandler = new ContextHandler($this->typedDataManager); + + $container = new ContainerBuilder(); + $container->set('typed_data_manager', $this->typedDataManager); + \Drupal::setContainer($container); + } + + /** + * @covers ::checkRequirements + * + * @dataProvider providerTestCheckRequirements + */ + public function testCheckRequirements($contexts, $requirements, $expected) { + $this->assertSame($expected, $this->contextHandler->checkRequirements($contexts, $requirements)); + } + + /** + * Provides data for testCheckRequirements(). + */ + public function providerTestCheckRequirements() { + $requirement_optional = $this->getMock('Drupal\Core\TypedData\DataDefinitionInterface'); + $requirement_optional->expects($this->atLeastOnce()) + ->method('isRequired') + ->will($this->returnValue(FALSE)); + + $requirement_any = $this->getMock('Drupal\Core\TypedData\DataDefinitionInterface'); + $requirement_any->expects($this->atLeastOnce()) + ->method('isRequired') + ->will($this->returnValue(TRUE)); + $requirement_any->expects($this->atLeastOnce()) + ->method('getDataType') + ->will($this->returnValue('any')); + $requirement_any->expects($this->atLeastOnce()) + ->method('getConstraints') + ->will($this->returnValue(array())); + + $context_any = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context_any->expects($this->atLeastOnce()) + ->method('getContextDefinition') + ->will($this->returnValue(array())); + + $requirement_specific = $this->getMock('Drupal\Core\TypedData\DataDefinitionInterface'); + $requirement_specific->expects($this->atLeastOnce()) + ->method('isRequired') + ->will($this->returnValue(TRUE)); + $requirement_specific->expects($this->atLeastOnce()) + ->method('getDataType') + ->will($this->returnValue('foo')); + $requirement_specific->expects($this->atLeastOnce()) + ->method('getConstraints') + ->will($this->returnValue(array('bar' => 'baz'))); + + $context_constraint_mismatch = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context_constraint_mismatch->expects($this->atLeastOnce()) + ->method('getContextDefinition') + ->will($this->returnValue(array('type' => 'foo'))); + $context_datatype_mismatch = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context_datatype_mismatch->expects($this->atLeastOnce()) + ->method('getContextDefinition') + ->will($this->returnValue(array('type' => 'fuzzy'))); + + $context_specific = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context_specific->expects($this->atLeastOnce()) + ->method('getContextDefinition') + ->will($this->returnValue(array('type' => 'foo', 'constraints' => array('bar' => 'baz')))); + + $data = array(); + $data[] = array(array(), array(), TRUE); + $data[] = array(array(), array($requirement_any), FALSE); + $data[] = array(array(), array($requirement_optional), TRUE); + $data[] = array(array(), array($requirement_any, $requirement_optional), FALSE); + $data[] = array(array($context_any), array($requirement_any), TRUE); + $data[] = array(array($context_constraint_mismatch), array($requirement_specific), FALSE); + $data[] = array(array($context_datatype_mismatch), array($requirement_specific), FALSE); + $data[] = array(array($context_specific), array($requirement_specific), TRUE); + + return $data; + } + + /** + * @covers ::getMatchingContexts + * + * @dataProvider providerTestGetMatchingContexts + */ + public function testGetMatchingContexts($contexts, $requirement, $expected = NULL) { + if (is_null($expected)) { + $expected = $contexts; + } + $this->assertSame($expected, $this->contextHandler->getMatchingContexts($contexts, $requirement)); + } + + /** + * Provides data for testGetMatchingContexts(). + */ + public function providerTestGetMatchingContexts() { + $requirement_any = $this->getMock('Drupal\Core\TypedData\DataDefinitionInterface'); + $requirement_any->expects($this->atLeastOnce()) + ->method('isRequired') + ->will($this->returnValue(TRUE)); + $requirement_any->expects($this->atLeastOnce()) + ->method('getDataType') + ->will($this->returnValue('any')); + $requirement_any->expects($this->atLeastOnce()) + ->method('getConstraints') + ->will($this->returnValue(array())); + $requirement_specific = $this->getMock('Drupal\Core\TypedData\DataDefinitionInterface'); + $requirement_specific->expects($this->atLeastOnce()) + ->method('isRequired') + ->will($this->returnValue(TRUE)); + $requirement_specific->expects($this->atLeastOnce()) + ->method('getDataType') + ->will($this->returnValue('foo')); + $requirement_specific->expects($this->atLeastOnce()) + ->method('getConstraints') + ->will($this->returnValue(array('bar' => 'baz'))); + + $context_any = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context_any->expects($this->atLeastOnce()) + ->method('getContextDefinition') + ->will($this->returnValue(array())); + $context_constraint_mismatch = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context_constraint_mismatch->expects($this->atLeastOnce()) + ->method('getContextDefinition') + ->will($this->returnValue(array('type' => 'foo'))); + $context_datatype_mismatch = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context_datatype_mismatch->expects($this->atLeastOnce()) + ->method('getContextDefinition') + ->will($this->returnValue(array('type' => 'fuzzy'))); + $context_specific = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context_specific->expects($this->atLeastOnce()) + ->method('getContextDefinition') + ->will($this->returnValue(array('type' => 'foo', 'constraints' => array('bar' => 'baz')))); + + $data = array(); + // No context will return no valid contexts. + $data[] = array(array(), $requirement_any); + // A context with a generic matching requirement is valid. + $data[] = array(array($context_any), $requirement_any); + // A context with a specific matching requirement is valid. + $data[] = array(array($context_specific), $requirement_specific); + + // A context with a mismatched constraint is invalid. + $data[] = array(array($context_constraint_mismatch), $requirement_specific, array()); + // A context with a mismatched datatype is invalid. + $data[] = array(array($context_datatype_mismatch), $requirement_specific, array()); + + return $data; + } + + /** + * @covers ::filterPluginDefinitionsByContexts + * + * @dataProvider providerTestFilterPluginDefinitionsByContexts + */ + public function testFilterPluginDefinitionsByContexts($contexts, $definitions, $expected, $typed_data_definition = NULL) { + if ($typed_data_definition) { + $this->typedDataManager->expects($this->atLeastOnce()) + ->method('getDefinition') + ->will($this->returnValueMap($typed_data_definition)); + } + + $this->assertSame($expected, $this->contextHandler->filterPluginDefinitionsByContexts($contexts, $definitions)); + } + + /** + * Provides data for testFilterPluginDefinitionsByContexts(). + */ + public function providerTestFilterPluginDefinitionsByContexts() { + $context = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context->expects($this->atLeastOnce()) + ->method('getContextDefinition') + ->will($this->returnValue(array('type' => 'expected_data_type', 'constraints' => array('expected_constraint_name' => 'expected_constraint_value')))); + + $data = array(); + + $plugins = array(); + // No context and no plugins, no plugins available. + $data[] = array(array(), $plugins, array()); + + $plugins = array('expected_plugin' => array()); + // No context, all plugins available. + $data[] = array(array(), $plugins, $plugins); + + $plugins = array('expected_plugin' => array('context' => array())); + // No context, all plugins available. + $data[] = array(array(), $plugins, $plugins); + + $plugins = array('expected_plugin' => array('context' => array('context1' => array('type' => 'expected_data_type')))); + // Missing context, no plugins available. + $data[] = array(array(), $plugins, array()); + // Satisfied context, all plugins available. + $data[] = array(array($context), $plugins, $plugins); + + $plugins = array('expected_plugin' => array('context' => array('context1' => array('type' => 'expected_data_type', 'constraints' => array('mismatched_constraint_name' => 'mismatched_constraint_value'))))); + // Mismatched constraints, no plugins available. + $data[] = array(array($context), $plugins, array()); + + $plugins = array('expected_plugin' => array('context' => array('context1' => array('type' => 'expected_data_type', 'constraints' => array('expected_constraint_name' => 'expected_constraint_value'))))); + // Satisfied context with constraint, all plugins available. + $data[] = array(array($context), $plugins, $plugins); + + $typed_data = array(array('expected_data_type', TRUE, array('required' => FALSE))); + // Optional unsatisfied context from TypedData, all plugins available. + $data[] = array(array(), $plugins, $plugins, $typed_data); + + $typed_data = array(array('expected_data_type', TRUE, array('required' => TRUE))); + // Required unsatisfied context from TypedData, no plugins available. + $data[] = array(array(), $plugins, array(), $typed_data); + + $typed_data = array(array('expected_data_type', TRUE, array('constraints' => array('mismatched_constraint_name' => 'mismatched_constraint_value'), 'required' => FALSE))); + // Optional mismatched constraint from TypedData, all plugins available. + $data[] = array(array(), $plugins, $plugins, $typed_data); + + $typed_data = array(array('expected_data_type', TRUE, array('constraints' => array('mismatched_constraint_name' => 'mismatched_constraint_value'), 'required' => TRUE))); + // Required mismatched constraint from TypedData, no plugins available. + $data[] = array(array(), $plugins, array(), $typed_data); + + $typed_data = array(array('expected_data_type', TRUE, array('constraints' => array('expected_constraint_name' => 'expected_constraint_value')))); + // Satisfied constraint from TypedData, all plugins available. + $data[] = array(array($context), $plugins, $plugins, $typed_data); + + $plugins = array( + 'unexpected_plugin' => array('context' => array('context1' => array('type' => 'unexpected_data_type', 'constraints' => array('mismatched_constraint_name' => 'mismatched_constraint_value')))), + 'expected_plugin' => array('context' => array('context2' => array('type' => 'expected_data_type'))), + ); + $typed_data = array( + array('unexpected_data_type', TRUE, array()), + array('expected_data_type', TRUE, array('constraints' => array('expected_constraint_name' => 'expected_constraint_value'))), + ); + // Context only satisfies one plugin. + $data[] = array(array($context), $plugins, array('expected_plugin' => $plugins['expected_plugin']), $typed_data); + + return $data; + } + + /** + * @covers ::applyContextMapping + */ + public function testApplyContextMapping() { + $context_hit = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context_hit->expects($this->atLeastOnce()) + ->method('getContextValue') + ->will($this->returnValue(array('foo'))); + $context_miss = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context_miss->expects($this->never()) + ->method('getContextValue'); + + $contexts = array( + 'hit' => $context_hit, + 'miss' => $context_miss, + ); + + $plugin = $this->getMock('Drupal\Component\Plugin\ContextAwarePluginInterface'); + $plugin->expects($this->once()) + ->method('getContextDefinitions') + ->will($this->returnValue(array('hit' => 'hit'))); + $plugin->expects($this->once()) + ->method('setContextValue') + ->with('hit', array('foo')); + + $this->contextHandler->applyContextMapping($plugin, $contexts); + } + + /** + * @covers ::applyContextMapping + */ + public function testApplyContextMappingConfigurable() { + $context = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context->expects($this->never()) + ->method('getContextValue'); + + $contexts = array( + 'name' => $context, + ); + + $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface'); + $plugin->expects($this->once()) + ->method('getContextDefinitions') + ->will($this->returnValue(array('hit' => 'hit'))); + $plugin->expects($this->never()) + ->method('setContextValue'); + + $this->contextHandler->applyContextMapping($plugin, $contexts); + } + + /** + * @covers ::applyContextMapping + */ + public function testApplyContextMappingConfigurableAssigned() { + $context = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context->expects($this->atLeastOnce()) + ->method('getContextValue') + ->will($this->returnValue(array('foo'))); + + $contexts = array( + 'name' => $context, + ); + + $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface'); + $plugin->expects($this->once()) + ->method('getContextDefinitions') + ->will($this->returnValue(array('hit' => 'hit'))); + $plugin->expects($this->once()) + ->method('setContextValue') + ->with('hit', array('foo')); + + $this->contextHandler->applyContextMapping($plugin, $contexts, array('name' => 'hit')); + } + + /** + * @covers ::applyContextMapping + * + * @expectedException \Drupal\Component\Plugin\Exception\ContextException + * @expectedExceptionMessage Assigned contexts were not satisfied: miss + */ + public function testApplyContextMappingConfigurableAssignedMiss() { + $context = $this->getMock('Drupal\Component\Plugin\Context\ContextInterface'); + $context->expects($this->never()) + ->method('getContextValue'); + + $contexts = array( + 'name' => $context, + ); + + $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface'); + $plugin->expects($this->once()) + ->method('getContextDefinitions') + ->will($this->returnValue(array('hit' => 'hit'))); + $plugin->expects($this->never()) + ->method('setContextValue'); + + $this->contextHandler->applyContextMapping($plugin, $contexts, array('name' => 'miss')); + } + +} + +interface TestConfigurableContextAwarePluginInterface extends ContextAwarePluginInterface, ConfigurablePluginInterface { +} -- GitLab