Commit 47e74965 authored by alexpott's avatar alexpott

Issue #2277981 by tim.plunkett, EclipseGc, fago: Provide a service for...

Issue #2277981 by tim.plunkett, EclipseGc, fago: Provide a service for handling context-aware plugins.
parent 49a90046
......@@ -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']
......
......@@ -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.
*
......
<?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());
}
<?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();
}
<?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))));
}
}
}
<?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());
}
......@@ -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']);
......
......@@ -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.
......
......@@ -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.
*/
......
<?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(),
);
}
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment