Commit 71364b1a authored by Dries's avatar Dries

Issue #1896076 by EclipseGc, fago: Added Contextual Plugins and supporting code.

parent 4d4facb2
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Context\Context.
*/
namespace Drupal\Component\Plugin\Context;
use Drupal\Component\Plugin\Exception\ContextException;
/**
* A generic context class for wrapping data a plugin needs to operate.
*/
class Context implements ContextInterface {
/**
* The value of the context.
*
* @var mixed
*/
protected $contextValue;
/**
* The definition to which a context must conform.
*
* @var array
*/
protected $contextDefinition;
/**
* Sets the contextDefinition for us without needing to call the setter.
*/
public function __construct(array $context_definition) {
$this->contextDefinition = $context_definition;
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::setContextValue().
*/
public function setContextValue($value) {
$value = $this->validate($value);
$this->contextValue = $value;
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::getContextValue().
*/
public function getContextValue() {
return $this->contextValue;
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::setContextDefinition().
*/
public function setContextDefinition(array $context_definition) {
$this->contextDefinition = $context_definition;
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::getContextDefinition().
*/
public function getContextDefinition() {
return $this->contextDefinition;
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::validate().
*
* The default validation method only supports instance of checks between the
* contextDefintion and the contextValue. Other formats of context
* definitions can be supported through a subclass.
*/
public function validate($value) {
// Check to make sure we have a class name, and that the passed context is
// an instance of that class name.
if (!empty($this->contextDefinition['class'])) {
if ($value instanceof $this->contextDefinition['class']) {
return $value;
}
throw new ContextException("The context passed was not an instance of {$this->contextDefinition['class']}.");
}
throw new ContextException("An error was encountered while trying to validate the context.");
}
}
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Context\ContextInterface.
*/
namespace Drupal\Component\Plugin\Context;
use Drupal\Component\Plugin\Exception\ContextException;
/**
* A generic context interface for wrapping data a plugin needs to operate.
*/
interface ContextInterface {
/**
* Sets the context value.
*
* @param mixed $value
* The value of this context, generally an object based upon the class
* matching the definition passed to setContextDefinition().
*/
public function setContextValue($value);
/**
* Gets the context value.
*
* @return mixed
* The currently set context value within this class.
*/
public function getContextValue();
/**
* Sets the definition that the context must conform to.
*
* @param mixed $contextDefinition
* A defining characteristic representation of the context against which
* that context can be validated. This is typically a class name, but could
* be extended to support other validation notation.
*/
public function setContextDefinition(array $contextDefinition);
/**
* Gets the provided definition that the context must conform to.
*
* @return mixed
* The defining characteristic representation of the context.
*/
public function getContextDefinition();
/**
* Validate the provided context value against the provided definition.
*
* @param mixed $value
* The context value that should be validated against the context
* definition.
*
* @return mixed
* Returns the context value passed to it. If it fails validation, an
* exception will be thrown.
*
* @throws \Drupal\Component\Plugin\Exception\ContextException
* If validation fails.
*/
public function validate($value);
}
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\ContextAwarePluginBase
*/
namespace Drupal\Component\Plugin;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\Context\Context;
/**
* Base class for plugins that are context aware.
*/
abstract class ContextAwarePluginBase extends PluginBase implements ContextAwarePluginInterface {
/**
* The data objects representing the context of this plugin.
*
* @var array
*/
protected $context;
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::getContextDefinitions().
*/
public function getContextDefinitions() {
$definition = $this->getDefinition();
return !empty($definition['context']) ? $definition['context'] : NULL;
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::getContextDefinition().
*/
public function getContextDefinition($key) {
$definition = $this->getDefinition();
if (empty($definition['context'][$key])) {
throw new PluginException("The $key context is not a valid context.");
}
return $definition['context'][$key];
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::getContexts().
*/
public function getContexts() {
$definitions = $this->getContextDefinitions();
// If there are no contexts defined by the plugin, return an empty array.
if (empty($definitions)) {
return array();
}
if (empty($this->context)) {
throw new PluginException("There are no set contexts.");
}
$contexts = array();
foreach (array_keys($definitions) as $key) {
if (empty($this->context[$key])) {
throw new PluginException("The $key context is not yet set.");
}
$contexts[$key] = $this->context[$key];
}
return $contexts;
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::getContext().
*/
public function getContext($key) {
// Check for a valid context definition.
$this->getContextDefinition($key);
// Check for a valid context value.
if (empty($this->context[$key])) {
throw new PluginException("The $key context is not yet set.");
}
return $this->context[$key];
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::getContextValues().
*/
public function getContextValues() {
$contexts = array();
foreach ($this->getContexts() as $key => $context) {
$contexts[$key] = $context->getContextValue();
}
return $contexts;
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::getContextValue().
*/
public function getContextValue($key) {
return $this->getContext($key)->getContextValue();
}
/**
* Implements \Drupal\Component\Plugin\ContextAwarePluginInterface::setContextValue().
*/
public function setContextValue($key, $value) {
$context_definition = $this->getContextDefinition($key);
$this->context[$key] = new Context($context_definition);
$this->context[$key]->setContextValue($value);
return $this;
}
}
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\ContextAwarePluginInterface.
*/
namespace Drupal\Component\Plugin;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\Context\Context;
/**
* Interface for defining context aware plugins.
*/
interface ContextAwarePluginInterface extends PluginInspectionInterface {
/**
* Gets the context definitions of the plugin.
*
* @return array|null
* The context definitions if set, otherwise NULL.
*/
public function getContextDefinitions();
/**
* Gets the a specific context definition of the plugin.
*
* @param string $key
* The name of the context in the plugin definition.
*
* @return mixed
* The definition against which the context value must validate.
*/
public function getContextDefinition($key);
/**
* Gets the defined contexts.
*
* @return array
* The set context objects.
*/
public function getContexts();
/**
* Gets a defined context.
*
* @param string $key
* The name of the context in the plugin configuration. This string is
* usually identical to the representative string in the plugin definition.
*
* @return \Drupal\Component\Plugin\Context\ContextInterface
* The context object.
*/
public function getContext($key);
/**
* Gets the values for all defined contexts.
*
* @return array
* The set context object values.
*/
public function getContextValues();
/**
* Gets the value for a defined context.
*
* @param string $key
* The name of the context in the plugin configuration. This string is
* usually identical to the representative string in the plugin definition.
*
* @return mixed
* The currently set context value.
*/
public function getContextValue($key);
/**
* Sets the value for a defined context.
*
* @param string $key
* The name of the context in the plugin definition.
* @param mixed $value
* The variable to set the context to. This should validate against the
* provided context definition.
*
* @return \Drupal\Component\Plugin\ContextAwarePluginInterface.
* A context aware plugin object for chaining.
*/
public function setContextValue($key, $value);
}
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Exception\ContextException.
*/
namespace Drupal\Component\Plugin\Exception;
use Exception;
/**
* An exception class to be thrown for context plugin exceptions.
*/
class ContextException extends Exception implements ExceptionInterface { }
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Context\Context.
*/
namespace Drupal\Core\Plugin\Context;
use Drupal\Component\Plugin\Context\Context as ComponentContext;
use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\TypedData\TypedDataManager;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* 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 {
/**
* Overrides \Drupal\Component\Plugin\Context\Context::getContextValue().
*/
public function getContextValue() {
$typed_value = parent::getContextValue();
// If the data is of a primitive type, directly return the plain value.
// That way, e.g. a string will be return as plain PHP string.
if ($typed_value instanceof \Drupal\Core\TypedData\TypedDataInterface) {
$type_definition = typed_data()->getDefinition($typed_value->getType());
if (!empty($type_definition['primitive type'])) {
return $typed_value->getValue();
}
}
return $typed_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
*/
public function getTypedContext() {
return parent::getContextValue();
}
/**
* Override for \Drupal\Component\Plugin\Context\Context::validate().
*/
public function validate($value) {
if (!empty($this->contextDefinition['type'])) {
$typed_data_manager = new TypedDataManager();
$typed_data = $typed_data_manager->create($this->contextDefinition, $value);
// If we do have a typed data definition, validate it and return the
// typed data instance instead.
$violations = $typed_data->validate();
if (count($violations) == 0) {
return $typed_data;
}
throw new ContextException("The context passed could not be validated through typed data.");
}
return parent::validate($value);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\ContextAwarePluginBase
*/
namespace Drupal\Core\Plugin;
use Drupal\Component\Plugin\ContextAwarePluginBase as PluginBase;
use Drupal\Core\Plugin\Context\Context;
/**
* Drupal specific class for plugins that use context.
*
* This class specifically overrides setContextValue to use the core version of
* the Context class. This code is exactly the same as what is in Component
* ContextAwarePluginBase but it is using a different Context class.
*/
abstract class ContextAwarePluginBase extends PluginBase {
/**
* Override of \Drupal\Component\Plugin\ContextAwarePluginBase::setContextValue().
*/
public function setContextValue($key, $value) {
$context_definition = $this->getContextDefinition($key);
$this->context[$key] = new Context($context_definition);
$this->context[$key]->setContextValue($value);
return $this;
}
}
<?php
/**
* @file
* Contains \Drupal\system\Tests\Plugin\ContextPluginTest.
*/
namespace Drupal\system\Tests\Plugin;
use Drupal\simpletest\DrupalUnitTestBase;
use Drupal\plugin_test\Plugin\MockBlockManager;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\Exception\ContextException;
/**
* Tests that context aware plugins function correctly.
*/
class ContextPluginTest extends DrupalUnitTestBase {
public static $modules = array('system', 'user', 'node');
public static function getInfo() {
return array(
'name' => 'Contextual Plugins',
'description' => 'Tests that contexts are properly set and working within plugins.',
'group' => 'Plugin API',
);
}
protected function setUp() {
parent::setUp();
$this->installSchema('node', 'node_type');
}
/**
* Tests basic context definition and value getters and setters.
*/
function testContext() {
$name = $this->randomName();
$manager = new MockBlockManager();
$plugin = $manager->createInstance('user_name');
// Create a node, add it as context, catch the exception.
$node = entity_create('node', array('title' => $name));
// Try to get a valid context that has not been set.
try {
$plugin->getContext('user');
$this->fail('The user context should not yet be set.');
}
catch (PluginException $e) {
$this->assertEqual($e->getMessage(), 'The user context is not yet set.');
}
// Try to get an invalid context.
try {
$plugin->getContext('node');
$this->fail('The node context should not be a valid context.');
}
catch (PluginException $e) {
$this->assertEqual($e->getMessage(), 'The node context is not a valid context.');
}
// Try to get a valid context value that has not been set.
try {
$plugin->getContextValue('user');
$this->fail('The user context should not yet be set.');
}
catch (PluginException $e) {
$this->assertEqual($e->getMessage(), 'The user context is not yet set.');
}
// Try to call a method of the plugin that requires context before it has
// been set.
try {
$plugin->getTitle();
$this->fail('The user context should not yet be set.');
}
catch (PluginException $e) {
$this->assertEqual($e->getMessage(), 'The user context is not yet set.');
}
// Try to get a context value that is not valid.
try {
$plugin->getContextValue('node');
$this->fail('The node context should not be a valid context.');
}
catch (PluginException $e) {
$this->assertEqual($e->getMessage(), 'The node context is not a valid context.');
}
// Try to pass the wrong class type as a context value.
try {
$plugin->setContextValue('user', $node);
$this->fail('The node context should fail validation for a user context.');
}
catch (ContextException $e) {
$this->assertEqual($e->getMessage(), 'The context passed was not an instance of Drupal\user\Plugin\Core\Entity\User.');
}
// Set an appropriate context value appropriately and check to make sure
// its methods work as expected.
$user = entity_create('user', array('name' => $name));
$plugin->setContextValue('user', $user);
$this->assertEqual($user->label(), $plugin->getTitle());
// Test the getContextDefinitions() method.
$this->assertIdentical($plugin->getContextDefinitions(), array('user' => array('class' => 'Drupal\user\Plugin\Core\Entity\User')));
// Test the getContextDefinition() method for a valid context.
$this->assertEqual($plugin->getContextDefinition('user'), array('class' => 'Drupal\user\Plugin\Core\Entity\User'));
// Test the getContextDefinition() method for an invalid context.
try {
$plugin->getContextDefinition('node');
$this->fail('The node context should not be a valid context.');
}
catch (PluginException $e) {
$this->assertEqual($e->getMessage(), 'The node context is not a valid context.');
}
// Test typed data context plugins.
$typed_data_plugin = $manager->createInstance('string_context');
// Try to get a valid context value that has not been set.
try {
$typed_data_plugin->getContextValue('string');
$this->fail('The string context should not yet be set.');
}
catch (PluginException $e) {
$this->assertEqual($e->getMessage(), 'The string context is not yet set.');
}
// Try to call a method of the plugin that requires a context value before
// it has been set.
try {
$typed_data_plugin->getTitle();
$this->fail('The string context should not yet be set.');
}
catch (PluginException $e) {
$this->assertEqual($e->getMessage(), 'The string context is not yet set.');