Commit 099f4146 authored by catch's avatar catch
Browse files

Issue #2932462 by phenaproxima, japerry, tim.plunkett: Add...

Issue #2932462 by phenaproxima, japerry, tim.plunkett: Add EntityContextDefinition for the 80% use case
parent 14a769b7
......@@ -116,10 +116,36 @@ public function __construct(array $values) {
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';
$class = $this->getDefinitionClass($values);
$this->definition = new $class($values['value'], $values['label'], $values['required'], $values['multiple'], $values['description'], $values['default_value']);
}
/**
* Determines the context definition class to use.
*
* If the annotation specifies a specific context definition class, we use
* that. Otherwise, we use \Drupal\Core\Plugin\Context\EntityContextDefinition
* if the data type starts with 'entity:', since it contains specialized logic
* specific to entities. Otherwise, we fall back to the generic
* \Drupal\Core\Plugin\Context\ContextDefinition class.
*
* @param array $values
* The annotation values.
*
* @return string
* The fully-qualified name of the context definition class.
*/
protected function getDefinitionClass(array $values) {
if (isset($values['class'])) {
return $values['class'];
}
if (strpos($values['value'], 'entity:') === 0) {
return 'Drupal\Core\Plugin\Context\EntityContextDefinition';
}
return 'Drupal\Core\Plugin\Context\ContextDefinition';
}
/**
* Returns the value of an annotation.
*
......
......@@ -3,11 +3,6 @@
namespace Drupal\Core\Plugin\Context;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
use Drupal\Core\Entity\Plugin\Validation\Constraint\BundleConstraint;
use Drupal\Core\Entity\Plugin\Validation\Constraint\EntityTypeConstraint;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
use Drupal\Core\TypedData\TypedDataTrait;
/**
......@@ -15,7 +10,10 @@
*/
class ContextDefinition implements ContextDefinitionInterface {
use DependencySerializationTrait;
use DependencySerializationTrait {
__sleep as traitSleep;
__wakeup as traitWakeup;
}
use TypedDataTrait;
......@@ -72,6 +70,38 @@ class ContextDefinition implements ContextDefinitionInterface {
*/
protected $constraints = [];
/**
* An EntityContextDefinition instance, for backwards compatibility.
*
* If this context is created with a data type that starts with 'entity:',
* this property will be an instance of EntityContextDefinition, and certain
* methods of this object will delegate to their overridden counterparts in
* $this->entityContextDefinition.
*
* This property should be kept private so that it is only accessible to this
* class for backwards compatibility reasons. It will be removed in Drupal 9.
*
* @deprecated
* Constructing a context definition for an entity type (i.e., the data type
* begins with 'entity:') is deprecated in Drupal 8.6.0. Instead, use
* the static factory methods of EntityContextDefinition to create context
* definitions for entity types, or the static ::create() method of this
* class for any other data type. See https://www.drupal.org/node/2976400
* for more information.
*
* @see ::__construct()
* @see ::__sleep()
* @see ::__wakeup()
* @see ::getConstraintObjects()
* @see ::getSampleValues()
* @see ::initializeEntityContextDefinition()
* @see https://www.drupal.org/node/2932462
* @see https://www.drupal.org/node/2976400
*
* @var \Drupal\Core\Plugin\Context\EntityContextDefinition
*/
private $entityContextDefinition;
/**
* Creates a new context definition.
*
......@@ -111,6 +141,11 @@ public function __construct($data_type = 'any', $label = NULL, $required = TRUE,
$this->isMultiple = $multiple;
$this->description = $description;
$this->defaultValue = $default_value;
if (strpos($data_type, 'entity:') === 0 && !($this instanceof EntityContextDefinition)) {
@trigger_error('Constructing a ContextDefinition object for an entity type is deprecated in Drupal 8.6.0. Use ' . __NAMESPACE__ . '\EntityContextDefinition instead. See https://www.drupal.org/node/2976400 for more information.', E_USER_DEPRECATED);
$this->initializeEntityContextDefinition();
}
}
/**
......@@ -274,7 +309,12 @@ public function isSatisfiedBy(ContextInterface $context) {
$values = [$context->getContextData()];
}
elseif ($definition instanceof self) {
$values = $definition->getSampleValues();
if ($this->entityContextDefinition) {
$values = $this->entityContextDefinition->getSampleValues();
}
else {
$values = $definition->getSampleValues();
}
}
else {
$values = [];
......@@ -309,33 +349,6 @@ public function isSatisfiedBy(ContextInterface $context) {
* The set of typed data object.
*/
protected function getSampleValues() {
// @todo Move the entity specific logic out of this class in
// https://www.drupal.org/node/2932462.
// Get the constraints from the context's definition.
$constraints = $this->getConstraintObjects();
// If constraints include EntityType, we generate an entity or adapter.
if (!empty($constraints['EntityType']) && $constraints['EntityType'] instanceof EntityTypeConstraint) {
$entity_type_manager = \Drupal::entityTypeManager();
$entity_type_id = $constraints['EntityType']->type;
$storage = $entity_type_manager->getStorage($entity_type_id);
// If the storage can generate a sample entity we might delegate to that.
if ($storage instanceof ContentEntityStorageInterface) {
if (!empty($constraints['Bundle']) && $constraints['Bundle'] instanceof BundleConstraint) {
foreach ($constraints['Bundle']->bundle as $bundle) {
// We have a bundle, we are bundleable and we can generate a sample.
yield EntityAdapter::createFromEntity($storage->createWithSampleValues($bundle));
}
return;
}
}
// Either no bundle, or not bundleable, so generate an entity adapter.
$definition = EntityDataDefinition::create($entity_type_id);
yield new EntityAdapter($definition);
return;
}
// No entity related constraints, so generate a basic typed data object.
yield $this->getTypedDataManager()->create($this->getDataDefinition());
}
......@@ -346,16 +359,13 @@ protected function getSampleValues() {
* A list of applied constraints for the context definition.
*/
protected function getConstraintObjects() {
$constraint_definitions = $this->getConstraints();
// @todo Move the entity specific logic out of this class in
// https://www.drupal.org/node/2932462.
// If the data type is an entity, manually add one to the constraints array.
if (strpos($this->getDataType(), 'entity:') === 0) {
$entity_type_id = substr($this->getDataType(), 7);
$constraint_definitions['EntityType'] = ['type' => $entity_type_id];
// If the backwards compatibility layer is present, delegate to that.
if ($this->entityContextDefinition) {
return $this->entityContextDefinition->getConstraintObjects();
}
$constraint_definitions = $this->getConstraints();
$validation_constraint_manager = $this->getTypedDataManager()->getValidationConstraintManager();
$constraints = [];
foreach ($constraint_definitions as $constraint_name => $constraint_definition) {
......@@ -365,4 +375,40 @@ protected function getConstraintObjects() {
return $constraints;
}
/**
* Implements magic __sleep() method.
*/
public function __sleep() {
return array_diff($this->traitSleep(), ['entityContextDefinition']);
}
/**
* Implements magic __wakeup() method.
*/
public function __wakeup() {
$this->traitWakeup();
if (strpos($this->getDataType(), 'entity:') === 0) {
$this->initializeEntityContextDefinition();
}
}
/**
* Initializes $this->entityContextDefinition for backwards compatibility.
*
* This method should be kept private so that it is only accessible to this
* class for backwards compatibility reasons. It will be removed in Drupal 9.
*
* @deprecated
*/
private function initializeEntityContextDefinition() {
$this->entityContextDefinition = EntityContextDefinition::create()
->setDataType($this->getDataType())
->setLabel($this->getLabel())
->setRequired($this->isRequired())
->setMultiple($this->isMultiple())
->setDescription($this->getDescription())
->setDefaultValue($this->getDefaultValue());
}
}
......@@ -27,7 +27,7 @@ interface ContextProviderInterface {
* $node = ...
*
* // Set that specific node as the value of the 'node' context.
* $context = new Context(new ContextDefinition('entity:node'), $node);
* $context = EntityContext::fromEntity($node);
* return ['node' => $context];
* @endcode
*
......@@ -62,7 +62,7 @@ public function getRuntimeContexts(array $unqualified_context_ids);
* // can be configured to use it. When the plugin, for example a block,
* // needs to evaluate the context, the value of this context will be
* // supplied by getRuntimeContexts().
* $context = new Context(new ContextDefinition('entity:node'));
* $context = EntityContext::fromEntityTypeId('node');
* return ['node' => $context];
* @endcode
*
......
<?php
namespace Drupal\Core\Plugin\Context;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Class to provide a specific entity context.
*/
class EntityContext extends Context {
/**
* Gets a context from an entity type ID.
*
* @param string $entity_type_id
* Entity type ID from which a definition will be derived.
* @param string $label
* (optional) The label of the context.
*
* @return static
*/
public static function fromEntityTypeId($entity_type_id, $label = NULL) {
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
return static::fromEntityType($entity_type, $label);
}
/**
* Gets a context from an entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* Entity type from which a definition will be derived.
* @param string $label
* (optional) The label of the context.
*
* @return static
*/
public static function fromEntityType(EntityTypeInterface $entity_type, $label = NULL) {
$definition = EntityContextDefinition::fromEntityType($entity_type);
if ($label) {
$definition->setLabel($label);
}
return new static($definition);
}
/**
* Gets a context object from an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* Entity that provides a context.
* @param string $label
* (optional) The label of the context.
*
* @return \Drupal\Core\Plugin\Context\EntityContext
*/
public static function fromEntity(EntityInterface $entity, $label = NULL) {
$context = static::fromEntityType($entity->getEntityType(), $label);
$context->setContextValue($entity);
return $context;
}
}
<?php
namespace Drupal\Core\Plugin\Context;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
use Drupal\Core\Entity\Plugin\Validation\Constraint\BundleConstraint;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
/**
* Defines a class to provide entity context definitions.
*/
class EntityContextDefinition extends ContextDefinition {
/**
* {@inheritdoc}
*/
public function __construct($data_type = 'any', $label = NULL, $required = TRUE, $multiple = FALSE, $description = NULL, $default_value = NULL) {
// Prefix the data type with 'entity:' so that this class can be constructed
// like so: new EntityContextDefinition('node')
if (strpos($data_type, 'entity:') !== 0) {
$data_type = "entity:$data_type";
}
parent::__construct($data_type, $label, $required, $multiple, $description, $default_value);
}
/**
* Returns the entity type ID of this context.
*
* @return string
* The entity type ID.
*/
protected function getEntityTypeId() {
// The data type is the entity type ID prefixed by 'entity:' (7 characters).
return substr($this->getDataType(), 7);
}
/**
* {@inheritdoc}
*/
protected function getConstraintObjects() {
if (!$this->getConstraint('EntityType')) {
$this->addConstraint('EntityType', [
'type' => $this->getEntityTypeId(),
]);
}
return parent::getConstraintObjects();
}
/**
* {@inheritdoc}
*/
protected function getSampleValues() {
// Get the constraints from the context's definition.
$constraints = $this->getConstraintObjects();
$entity_type_manager = \Drupal::entityTypeManager();
$entity_type_id = $this->getEntityTypeId();
$storage = $entity_type_manager->getStorage($entity_type_id);
// If the storage can generate a sample entity we might delegate to that.
if ($storage instanceof ContentEntityStorageInterface) {
if (!empty($constraints['Bundle']) && $constraints['Bundle'] instanceof BundleConstraint) {
foreach ($constraints['Bundle']->getBundleOption() as $bundle) {
// We have a bundle, we are bundleable and we can generate a sample.
yield EntityAdapter::createFromEntity($storage->createWithSampleValues($bundle));
}
return;
}
}
// Either no bundle, or not bundleable, so generate an entity adapter.
$definition = EntityDataDefinition::create($entity_type_id);
yield new EntityAdapter($definition);
}
/**
* Creates a context definition from a given entity type ID.
*
* @param string $entity_type_id
* The entity type ID from which to derive a context definition.
*
* @return static
*/
public static function fromEntityTypeId($entity_type_id) {
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
return static::fromEntityType($entity_type);
}
/**
* Creates a context definition from a given entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type from which to derive a context definition.
*
* @return static
*/
public static function fromEntityType(EntityTypeInterface $entity_type) {
return new static('entity:' . $entity_type->id(), $entity_type->getLabel());
}
/**
* Creates a context definition from a given entity object.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity from which to derive a context definition.
*
* @return static
*/
public static function fromEntity(EntityInterface $entity) {
return static::fromEntityType($entity->getEntityType());
}
}
......@@ -4,9 +4,8 @@
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Session\AccountInterface;
/**
......@@ -47,9 +46,8 @@ public function __construct(AccountInterface $account, EntityManagerInterface $e
public function getRuntimeContexts(array $unqualified_context_ids) {
$current_user = $this->userStorage->load($this->account->id());
$context1 = new Context(new ContextDefinition('entity:user', 'User A'), $current_user);
$context2 = new Context(new ContextDefinition('entity:user', 'User B'), $current_user);
$context1 = EntityContext::fromEntity($current_user, 'User A');
$context2 = EntityContext::fromEntity($current_user, 'User B');
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['user']);
......
......@@ -5,8 +5,7 @@
use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
......@@ -137,6 +136,7 @@ protected function contextRepository() {
public function buildMultiple(array $entities) {
$build_list = parent::buildMultiple($entities);
/** @var \Drupal\Core\Entity\EntityInterface $entity */
foreach ($entities as $id => $entity) {
$sections = $this->getRuntimeSections($entity);
if ($sections) {
......@@ -150,9 +150,10 @@ public function buildMultiple(array $entities) {
// Bypass ::getContexts() in order to use the runtime entity, not a
// sample entity.
$contexts = $this->contextRepository()->getAvailableContexts();
// @todo Use EntityContextDefinition after resolving
// https://www.drupal.org/node/2932462.
$contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity);
$label = new TranslatableMarkup('@entity being viewed', [
'@entity' => $entity->getEntityType()->getSingularLabel(),
]);
$contexts['layout_builder.entity'] = EntityContext::fromEntity($entity, $label);
foreach ($sections as $delta => $section) {
$build_list[$id]['_layout_builder'][$delta] = $section->toRenderArray($contexts);
}
......
......@@ -9,7 +9,7 @@
use Drupal\Core\Field\FieldConfigInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -118,9 +118,7 @@ public function getDerivativeDefinitions($base_plugin_definition) {
// unavailable to place in the block UI.
$derivative['_block_ui_hidden'] = !$field_definition->isDisplayConfigurable('view');
// @todo Use EntityContextDefinition after resolving
// https://www.drupal.org/node/2932462.
$context_definition = new ContextDefinition('entity:' . $entity_type_id, $entity_type_labels[$entity_type_id], TRUE);
$context_definition = EntityContextDefinition::fromEntityTypeId($entity_type_id)->setLabel($entity_type_labels[$entity_type_id]);
$context_definition->addConstraint('Bundle', [$bundle]);
$derivative['context'] = [
'entity' => $context_definition,
......
......@@ -8,9 +8,7 @@
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Url;
use Drupal\field_ui\FieldUI;
use Drupal\layout_builder\DefaultsSectionStorageInterface;
......@@ -255,12 +253,9 @@ public function getSectionListFromId($id) {
public function getContexts() {
$display = $this->getDisplay();
$entity = $this->sampleEntityGenerator->get($display->getTargetEntityTypeId(), $display->getTargetBundle());
$context_label = new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()]);
// @todo Use EntityContextDefinition after resolving
// https://www.drupal.org/node/2932462.
$contexts = [];
$contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", $context_label), $entity);
$contexts['layout_builder.entity'] = EntityContext::fromEntity($entity);
return $contexts;
}
......
......@@ -8,9 +8,7 @@
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Url;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\OverridesSectionStorageInterface;
......@@ -213,9 +211,7 @@ public function getLayoutBuilderUrl() {
*/
public function getContexts() {
$entity = $this->getEntity();
// @todo Use EntityContextDefinition after resolving
// https://www.drupal.org/node/2932462.
$contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity);
$contexts['layout_builder.entity'] = EntityContext::fromEntity($entity);
return $contexts;
}
......
......@@ -10,7 +10,7 @@
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Session\AccountInterface;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\layout_builder\Plugin\Block\FieldBlock;
......@@ -207,7 +207,7 @@ protected function getTestBlock(ProphecyInterface $entity_prophecy, array $confi
'admin_label' => 'Test Block',
'bundles' => ['entity_test'],
'context' => [
'entity' => new ContextDefinition('entity:entity_test', 'Test', TRUE),
'entity' => EntityContextDefinition::fromEntityTypeId('entity_test')->setLabel('Test'),
],
];
$formatter_manager = $this->prophesize(FormatterPluginManager::class);
......
......@@ -4,8 +4,9 @@
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\node\Entity\Node;
use Drupal\Core\StringTranslation\StringTranslationTrait;
......@@ -39,7 +40,7 @@ public function __construct(RouteMatchInterface $route_match) {
*/
public function getRuntimeContexts(array $unqualified_context_ids) {
$result = [];
$context_definition = new ContextDefinition('entity:node', NULL, FALSE);
$context_definition = EntityContextDefinition::create('node')->setRequired(FALSE);
$value = NULL;
if (($route_object = $this->routeMatch->getRouteObject()) && ($route_contexts = $route_object->getOption('parameters')) && isset($route_contexts['node'])) {
if ($node = $this->routeMatch->getParameter('node')) {
......@@ -65,7 +66,7 @@ public function getRuntimeContexts(array $unqualified_context_ids) {
* {@inheritdoc}
*/
public function getAvailableContexts() {
$context = new Context(new ContextDefinition('entity:node', $this->t('Node from URL')));
$context = EntityContext::fromEntityTypeId('node', $this->t('Node from URL'));
return ['node' => $context];
}
......
......@@ -7,6 +7,7 @@
use Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator;
use Drupal\Component\Plugin\Factory\ReflectionFactory;
use Drupal\Core\Plugin\Context\ContextDefinition;