Commit 3a1f6fd9 authored by webchick's avatar webchick

Issue #2281635 by fubhy, er.pushpinderrana, EclipseGc, blueminds,...

Issue #2281635 by fubhy, er.pushpinderrana, EclipseGc, blueminds, tim.plunkett: \Drupal\Core\Plugin\Context\Context should use a ContextDefinition class instead of arrays.
parent 8a44b5c7
......@@ -26,14 +26,17 @@ class Context implements ContextInterface {
/**
* The definition to which a context must conform.
*
* @var array
* @var \Drupal\Component\Plugin\Context\ContextDefinitionInterface
*/
protected $contextDefinition;
/**
* Sets the contextDefinition for us without needing to call the setter.
*
* @param \Drupal\Component\Plugin\Context\ContextDefinitionInterface $context_definition
* The context definition.
*/
public function __construct(array $context_definition) {
public function __construct(ContextDefinitionInterface $context_definition) {
$this->contextDefinition = $context_definition;
}
......@@ -48,13 +51,22 @@ public function setContextValue($value) {
* Implements \Drupal\Component\Plugin\Context\ContextInterface::getContextValue().
*/
public function getContextValue() {
// Support optional contexts.
if (!isset($this->contextValue)) {
$definition = $this->getContextDefinition();
if ($definition->isRequired()) {
$type = $definition->getDataType();
throw new ContextException(sprintf("The %s context is required and not present.", $type));
}
return NULL;
}
return $this->contextValue;
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::setContextDefinition().
* {@inheritdoc}
*/
public function setContextDefinition(array $context_definition) {
public function setContextDefinition(ContextDefinitionInterface $context_definition) {
$this->contextDefinition = $context_definition;
}
......
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Context\ContextDefinitionInterface.
*/
namespace Drupal\Component\Plugin\Context;
/**
* Interface for context definitions.
*/
interface ContextDefinitionInterface {
/**
* Returns a human readable label.
*
* @return string
* The label.
*/
public function getLabel();
/**
* Sets the human readable label.
*
* @param string $label
* The label to set.
*
* @return $this
*/
public function setLabel($label);
/**
* Returns a human readable description.
*
* @return string|null
* The description, or NULL if no description is available.
*/
public function getDescription();
/**
* Sets the human readable description.
*
* @param string|null $description
* The description to set.
*
* @return $this
*/
public function setDescription($description);
/**
* Returns the data type needed by the context.
*
* If the context is multiple-valued, this represents the type of each value.
*
* @return string
* The data type.
*/
public function getDataType();
/**
* Sets the data type needed by the context.
*
* @param string $data_type
* The data type to set.
*
* @return $this
*/
public function setDataType($data_type);
/**
* Returns whether the data is multi-valued, i.e. a list of data items.
*
* @return bool
* Whether the data is multi-valued; i.e. a list of data items.
*/
public function isMultiple();
/**
* Sets whether the data is multi-valued.
*
* @param bool $multiple
* (optional) Whether the data is multi-valued. Defaults to TRUE.
*
* @return $this
*/
public function setMultiple($multiple = TRUE);
/**
* Determines whether the context is required.
*
* For required data a non-NULL value is mandatory.
*
* @return bool
* Whether a data value is required.
*/
public function isRequired();
/**
* Sets whether the data is required.
*
* @param bool $required
* (optional) Whether the data is multi-valued. Defaults to TRUE.
*
* @return $this
*/
public function setRequired($required = TRUE);
/**
* Returns an array of validation constraints.
*
* @return array
* An array of validation constraint definitions, keyed by constraint name.
* Each constraint definition can be used for instantiating
* \Symfony\Component\Validator\Constraint objects.
*/
public function getConstraints();
/**
* Sets the array of validation constraints.
*
* NOTE: This will override any previously set constraints. In most cases
* ContextDefinitionInterface::addConstraint() should be used instead.
*
* @param array $constraints
* The array of constraints.
*
* @return $this
*
* @see self::addConstraint()
*/
public function setConstraints(array $constraints);
/**
* Adds a validation constraint.
*
* @param string $constraint_name
* The name of the constraint to add, i.e. its plugin id.
* @param array|null $options
* The constraint options as required by the constraint plugin, or NULL.
*
* @return $this
*/
public function addConstraint($constraint_name, $options = NULL);
/**
* Returns a validation constraint.
*
* @param string $constraint_name
* The name of the the constraint, i.e. its plugin id.
*
* @return array
* A validation constraint definition which can be used for instantiating a
* \Symfony\Component\Validator\Constraint object.
*/
public function getConstraint($constraint_name);
}
......@@ -33,13 +33,11 @@ public function getContextValue();
/**
* Sets the definition that the context must conform to.
*
* @param array $contextDefinition
* @param \Drupal\Component\Plugin\Context\ContextDefinitionInterface $context_definition
* A defining characteristic representation of the context against which
* that context can be validated. This is typically an array having a
* class name set under the 'class' key, but it could be extended to support
* other notations.
* that context can be validated.
*/
public function setContextDefinition(array $contextDefinition);
public function setContextDefinition(ContextDefinitionInterface $context_definition);
/**
* Gets the provided definition that the context must conform to.
......
......@@ -7,10 +7,10 @@
namespace Drupal\Component\Plugin;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\Context\ContextInterface;
use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Component\Plugin\Context\Context;
use Symfony\Component\Validator\ConstraintViolationList;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
/**
* Base class for plugins that are context aware.
......@@ -20,7 +20,7 @@
/**
* The data objects representing the context of this plugin.
*
* @var array
* @var \Drupal\Component\Plugin\Context\ContextInterface[]
*/
protected $context;
......@@ -55,7 +55,7 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::getContextDefinitions().
* {@inheritdoc}
*/
public function getContextDefinitions() {
$definition = $this->getPluginDefinition();
......@@ -63,50 +63,47 @@ public function getContextDefinitions() {
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::getContextDefinition().
* {@inheritdoc}
*/
public function getContextDefinition($name) {
$definition = $this->getPluginDefinition();
if (empty($definition['context'][$name])) {
throw new PluginException("The $name context is not a valid context.");
throw new ContextException(sprintf("The %s context is not a valid context.", $name));
}
return $definition['context'][$name];
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::getContexts().
* {@inheritdoc}
*/
public function getContexts() {
$definitions = $this->getContextDefinitions();
if ($definitions && empty($this->context)) {
throw new PluginException("There are no set contexts.");
}
$contexts = array();
foreach (array_keys($definitions) as $name) {
if (empty($this->context[$name])) {
throw new PluginException("The $name context is not yet set.");
}
$contexts[$name] = $this->context[$name];
// Make sure all context objects are initialized.
foreach ($this->getContextDefinitions() as $name => $definition) {
$this->getContext($name);
}
return $contexts;
return $this->context;
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::getContext().
* {@inheritdoc}
*/
public function getContext($name) {
// Check for a valid context definition.
$this->getContextDefinition($name);
// Check for a valid context value.
if (!isset($this->context[$name])) {
throw new PluginException("The $name context is not yet set.");
$this->context[$name] = new Context($this->getContextDefinition($name));
}
return $this->context[$name];
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::getContextValues().
* {@inheritdoc}
*/
public function setContext($name, ContextInterface $context) {
$this->context[$name] = $context;
}
/**
* {@inheritdoc}
*/
public function getContextValues() {
$values = array();
......@@ -117,36 +114,30 @@ public function getContextValues() {
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::getContextValue().
* {@inheritdoc}
*/
public function getContextValue($name) {
return $this->getContext($name)->getContextValue();
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::setContextValue().
* {@inheritdoc}
*/
public function setContextValue($name, $value) {
$context_definition = $this->getContextDefinition($name);
$this->context[$name] = new Context($context_definition);
$this->context[$name]->setContextValue($value);
$this->getContext($name)->setContextValue($value);
return $this;
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::valdidateContexts().
* {@inheritdoc}
*/
public function validateContexts() {
$violations = new ConstraintViolationList();
// @todo: Implement symfony validator API to let the validator traverse
// and set property paths accordingly.
foreach ($this->getContextDefinitions() as $name => $definition) {
// Validate any set values.
if (isset($this->context[$name])) {
$violations->addAll($this->context[$name]->validate());
}
// @todo: If no value is set, make sure any mapping is validated.
foreach ($this->getContexts() as $context) {
$violations->addAll($context->validate());
}
return $violations;
}
......
......@@ -7,7 +7,7 @@
namespace Drupal\Component\Plugin;
use Drupal\Component\Plugin\Exception\PluginException;
use \Drupal\Component\Plugin\Context\ContextInterface;
/**
* Interface for defining context aware plugins.
......@@ -36,7 +36,7 @@ public function getContextDefinitions();
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the requested context is not defined.
*
* @return array
* @return \Drupal\Component\Plugin\Context\ContextDefinitionInterface.
* The definition against which the context value must validate.
*/
public function getContextDefinition($name);
......@@ -89,6 +89,16 @@ public function getContextValues();
*/
public function getContextValue($name);
/**
* Set a context on this plugin.
*
* @param string $name
* The name of the context in the plugin configuration.
* @param \Drupal\Component\Plugin\Context\ContextInterface $context
* The context object to set.
*/
public function setContext($name, ContextInterface $context);
/**
* Sets the value for a defined context.
*
......
<?php
/**
* @file
* Contains \Drupal\Core\Annotation\ContextDefinition.
*/
namespace Drupal\Core\Annotation;
/**
* @defgroup plugin_context context definition plugin metadata
*
* @{
* When providing plugin annotations, contexts can be defined to support UI
* interactions through providing limits, and mapping contexts to appropriate
* plugins. Context definitions can be provided as such:
* @code
* context = {
* "node" = @ContextDefinition("entity:node")
* }
* @endcode
* Remove spaces after @ in your actual plugin - these are put into this sample
* code so that it is not recognized as an annotation.
*
* To add a label to a context definition use the "label" key:
* @code
* context = {
* "node" = @ContextDefinition("entity:node", label = @Translation("Node"))
* }
* @endcode
*
* Contexts are required unless otherwise specified. To make an optional
* context use the "required" key:
* @code
* context = {
* "node" = @ContextDefinition("entity:node", required = FALSE, label = @Translation("Node"))
* }
* @endcode
*
* To define multiple contexts, simply provide different key names in the
* context array:
* @code
* context = {
* "artist" = @ContextDefinition("entity:node", label = @Translation("Artist")),
* "album" = @ContextDefinition("entity:node", label = @Translation("Album"))
* }
* @endcode
* @}
*/
use Drupal\Component\Annotation\Plugin;
/**
* Defines a context definition annotation object.
*
* Some plugins require various data contexts in order to function. This class
* supports that need by allowing the contexts to be easily defined within an
* annotation and return a ContextDefinitionInterface implementing class.
*
* @Annotation
*
* @ingroup plugin_context
*/
class ContextDefinition extends Plugin {
/**
* The ContextDefinitionInterface object.
*
* @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface
*/
protected $definition;
/**
* Constructs a new context definition object.
*
* @param array $values
* An associative array with the following keys:
* - value: The required data type.
* - label: (optional) The UI label of this context definition.
* - required: (optional) Whether the context definition is required.
* - multiple: (optional) Whether the context definition is multivalue.
* - description: (optional) The UI description of this context definition.
* - class: (optional) A custom ContextDefinitionInterface class.
*
* @throws \Exception
* Thrown when the class key is specified with a non
* ContextDefinitionInterface implementing class.
*/
public function __construct(array $values) {
$values += array(
'required' => TRUE,
'multiple' => FALSE,
'label' => NULL,
'description' => NULL,
);
if (isset($values['class']) && !in_array('Drupal\Core\Plugin\Context\ContextDefinitionInterface', class_implements($values['class']))) {
throw new \Exception('ContextDefinition class must implement \Drupal\Core\Plugin\Context\ContextDefinitionInterface.');
}
$class = isset($values['class']) ? $values['class'] : 'Drupal\Core\Plugin\Context\ContextDefinition';
$this->definition = new $class($values['value'], $values['label'], $values['required'], $values['multiple'], $values['description']);
}
/**
* Returns the value of an annotation.
*
* @return \Drupal\Core\Plugin\Context\ContextDefinitionInterface
*/
public function get() {
return $this->definition;
}
}
......@@ -7,7 +7,7 @@
namespace Drupal\Core\Condition;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\Exception\ContextException;
/**
* Resolves a set of conditions.
......@@ -30,7 +30,7 @@ protected function resolveConditions($conditions, $condition_logic) {
try {
$pass = $condition->execute();
}
catch (PluginException $e) {
catch (ContextException $e) {
// If a condition is missing context, consider that a fail.
$pass = FALSE;
}
......
......@@ -50,6 +50,14 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
*/
public function createInstance($plugin_id, array $configuration = array()) {
$plugin = $this->factory->createInstance($plugin_id, $configuration);
// If we receive any context values via config set it into the plugin.
if (!empty($configuration['context'])) {
foreach ($configuration['context'] as $name => $context) {
$plugin->setContextValue($name, $context);
}
}
return $plugin->setExecutableManager($this);
}
......
......@@ -20,6 +20,13 @@
*/
abstract class ConditionPluginBase extends ExecutablePluginBase implements ConditionInterface {
/**
* {@inheritdoc}
*/
public static function contextDefinitions() {
return [];
}
/**
* {@inheritdoc}
*/
......
......@@ -8,84 +8,99 @@
namespace Drupal\Core\Plugin\Context;
use Drupal\Component\Plugin\Context\Context as ComponentContext;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\ListInterface;
use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Component\Utility\String;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\TypedData\TypedDataTrait;
/**
* A Drupal specific context wrapper class.
*
* The validate method is specifically overridden in order to support typed
* data definitions instead of just class names in the contextual definitions
* of plugins that extend ContextualPluginBase.
*/
class Context extends ComponentContext {
class Context extends ComponentContext implements ContextInterface {
use TypedDataTrait;
/**
* Overrides \Drupal\Component\Plugin\Context\Context::getContextValue().
* The data associated with the context.
*
* @var \Drupal\Core\TypedData\TypedDataInterface
*/
protected $contextData;
/**
* The definition to which a context must conform.
*
* @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface
*/
protected $contextDefinition;
/**
* {@inheritdoc}
*/
public function getContextValue() {
$typed_value = parent::getContextValue();
// If the typed data is complex, pass it on as typed data. Else pass on its
// plain value, such that e.g. a string will be directly returned as PHP
// string.
$is_complex = $typed_value instanceof ComplexDataInterface;
if (!$is_complex && $typed_value instanceof ListInterface) {
$is_complex = $typed_value[0] instanceof ComplexDataInterface;
if (!isset($this->contextData)) {
$definition = $this->getContextDefinition();
if ($definition->isRequired()) {
$type = $definition->getDataType();
throw new ContextException(String::format("The @type context is required and not present.", array('@type' => $type)));
}
return NULL;
}
if ($typed_value instanceof TypedDataInterface && !$is_complex) {
return $typed_value->getValue();
// Special case entities.
// @todo: Remove once entities do not implemented TypedDataInterface.
if ($this->contextData instanceof ContentEntityInterface) {
return $this->contextData;
}
return $typed_value;
return $this->contextData->getValue();
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::setContextValue().
* {@inheritdoc}
*/
public function setContextValue($value) {
// Make sure the value set is a typed data object.
if (!empty($this->contextDefinition['type']) && !$value instanceof TypedDataInterface) {
$value = \Drupal::typedDataManager()->create(new DataDefinition($this->contextDefinition), $value);
if ($value instanceof TypedDataInterface) {
return $this->setContextData($value);
}
else {
return $this->setContextData($this->getTypedDataManager()->create($this->contextDefinition->getDataDefinition(), $value));
}
parent::setContextValue($value);
}
/**
* Gets the context value as typed data object.
*
* parent::getContextValue() does not do all the processing required to
* return plain value of a TypedData object. This class overrides that method
* to return the appropriate values from TypedData objects, but the object
* itself can be useful as well, so this method is provided to allow for
* access to the TypedData object. Since parent::getContextValue() already
* does all the processing we need, we simply proxy to it here.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* {@inheritdoc}
*/
public function getTypedContext() {
return parent::getContextValue();
public function getConstraints() {
return $this->contextDefinition->getConstraints();
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::getConstraints().
* {@inheritdoc}
*/
public function getConstraints() {
if (!empty($this->contextDefinition['type'])) {
// If we do have typed data, leverage it for getting constraints.
return $this->getTypedContext()->getConstraints();
}
return parent::getConstraints();
public function getContextData() {
return $this->contextData;
}
/**
* {@inheritdoc}
*/
public function setContextData(TypedDataInterface $data) {
$this->contextData = $data;
return $this;
}
/**
* Overrides \Drupal\Component\Plugin\Context\Context::getConstraints().
* {@inheritdoc}
*/
public function getContextDefinition() {
return $this->contextDefinition;
}
/**
* {@inheritdoc}
*/
public function validate() {
// If the context is typed data, defer to its validation.
if (!empty($this->contextDefinition['type'])) {
return $this->getTypedContext()->validate();
}
return parent::validate();
return $this->getContextData()->validate();
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Context\ContextDefinition.
*/