diff --git a/core/core.services.yml b/core/core.services.yml index 21a3e590ba..b32933dc8d 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -339,7 +339,7 @@ services: - [setValidationConstraintManager, ['@validation.constraint']] context.handler: class: Drupal\Core\Plugin\Context\ContextHandler - arguments: ['@typed_data_manager'] + arguments: ['@typed_data_manager', '@entity_type.manager', '@entity_type.bundle.info'] context.repository: class: Drupal\Core\Plugin\Context\LazyContextRepository arguments: ['@service_container'] diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/BundleConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/BundleConstraintValidator.php index e904a96663..39e99afc5e 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/BundleConstraintValidator.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/BundleConstraintValidator.php @@ -2,7 +2,6 @@ namespace Drupal\Core\Entity\Plugin\Validation\Constraint; -use Drupal\Core\Plugin\Plugin\DataType\DataTypeWrapper; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -19,17 +18,6 @@ public function validate($entity, Constraint $constraint) { return; } - // Validate against wrapper bundle constraint when necessary. - if ($entity instanceof DataTypeWrapper) { - $bundles = $entity->getDataDefinition()->getConstraint('Bundle'); - $bundle = $bundles ? array_shift($bundles) : 0; - if ($bundle && !in_array($bundle, $constraint->getBundleOption())) { - $this->context->addViolation($constraint->message, array('%bundle' => implode(', ', $constraint->getBundleOption()))); - } - // $entity is not an entity, so we should not continue. - return; - } - if (!in_array($entity->bundle(), $constraint->getBundleOption())) { $this->context->addViolation($constraint->message, ['%bundle' => implode(', ', $constraint->getBundleOption())]); } diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php index 35c8f2bebf..b1fee2a294 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php @@ -2,7 +2,6 @@ namespace Drupal\Core\Entity\Plugin\Validation\Constraint; -use Drupal\Core\Entity\EntityInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -15,8 +14,8 @@ class EntityChangedConstraintValidator extends ConstraintValidator { * {@inheritdoc} */ public function validate($entity, Constraint $constraint) { - // Non-entities can be passed and should not be validated. - if (isset($entity) && $entity instanceof EntityInterface) { + if (isset($entity)) { + /** @var \Drupal\Core\Entity\EntityInterface $entity */ if (!$entity->isNew()) { $saved_entity = \Drupal::entityManager()->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id()); // A change to any other translation must add a violation to the current diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityTypeConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityTypeConstraintValidator.php index ddfbeb649f..89c5e8ead7 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityTypeConstraintValidator.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityTypeConstraintValidator.php @@ -2,7 +2,6 @@ namespace Drupal\Core\Entity\Plugin\Validation\Constraint; -use Drupal\Core\Plugin\Plugin\DataType\DataTypeWrapper; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -19,15 +18,6 @@ public function validate($entity, Constraint $constraint) { return; } - // Validate against wrapper EntityType constraint when necessary. - if ($entity instanceof DataTypeWrapper) { - if ($entity->getDataDefinition()->getConstraint('EntityType') != $constraint->type) { - $this->context->addViolation($constraint->message, array('%type' => $constraint->type)); - // $entity is not an entity, so we should not continue. - return; - } - } - /** @var $entity \Drupal\Core\Entity\EntityInterface */ if ($entity->getEntityTypeId() != $constraint->type) { $this->context->addViolation($constraint->message, ['%type' => $constraint->type]); diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php index e1503fcaef..f26f101831 100644 --- a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php @@ -4,6 +4,13 @@ use Drupal\Component\Plugin\Exception\ContextException; use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\Core\Entity\ContentEntityStorageInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +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\Plugin\ContextAwarePluginInterface; use Drupal\Core\TypedData\TypedDataManagerInterface; @@ -20,12 +27,33 @@ class ContextHandler implements ContextHandlerInterface { protected $typedDataManager; /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The entity type bundle information object. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $entityTypeBundleInfo; + + /** * ContextHandler constructor. * * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager + * The typed data manager. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The entity type bundle information object. */ - public function __construct(TypedDataManagerInterface $typed_data_manager) { + public function __construct(TypedDataManagerInterface $typed_data_manager, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info) { $this->typedDataManager = $typed_data_manager; + $this->entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; } /** @@ -70,22 +98,9 @@ public function getMatchingContexts(array $contexts, ContextDefinitionInterface // If the definition has constraints, validate the contexts against them. if ($definition->getConstraints()) { - $constraints = []; - foreach ($definition->getConstraints() as $constraint_name => $constraint_settings) { - $constraints[] = $this->typedDataManager->getValidationConstraintManager()->create($constraint_name, $constraint_settings); - } - if ($context->hasContextValue()) { - $typed_data_definition = $context->getContextData(); - } - else { - $typed_data_data_definition = $this->typedDataManager->createDataDefinition('data_type_wrapper'); - foreach ($context_definition->getDataDefinition()->getConstraints() as $constraint => $constraint_settings) { - $typed_data_data_definition->addConstraint($constraint, $constraint_settings); - } - $typed_data_definition = $this->typedDataManager->create($typed_data_data_definition); - } + $typed_data_definition = $this->getTypedDataFromContext($context); $validator = $this->typedDataManager->getValidator(); - $violations = $validator->validate($typed_data_definition, $constraints); + $violations = $validator->validate($typed_data_definition, array_values($this->getConstraintsFromContextDefinition($definition))); // Contexts without any violations are valid. return !$violations->count(); } @@ -150,4 +165,64 @@ public function applyContextMapping(ContextAwarePluginInterface $plugin, $contex } } + /** + * Extracts an array of constraints for a context definition object. + * + * @param \Drupal\Core\Plugin\Context\ContextDefinitionInterface $definition + * The context definition object. + * + * @return \Symfony\Component\Validator\Constraint[] + * A list of applied constraints for the context definition. + */ + protected function getConstraintsFromContextDefinition(ContextDefinitionInterface $definition) { + $constraints = []; + // If the data type is an entity, but doesn't provide a corresponding + // constraint, we need to manually add one to the constraints array. + if (strpos($definition->getDataType(), 'entity:') === 0 && !in_array('EntityType', array_keys($definition->getConstraints()))) { + $entity_type = substr($definition->getDataType(), 7); + $constraints['EntityType'] = $this->typedDataManager->getValidationConstraintManager()->create('EntityType', ['type' => $entity_type]); + } + foreach ($definition->getConstraints() as $constraint_name => $constraint_settings) { + $constraints[$constraint_name] = $this->typedDataManager->getValidationConstraintManager()->create($constraint_name, $constraint_settings); + } + return $constraints; + } + + /** + * Provides a reliable TypedData object from a context or its definition. + * + * @param \Drupal\Core\Plugin\Context\ContextInterface $context + * The context from which to generate a TypedData object. + * + * @return \Drupal\Core\TypedData\TypedDataInterface + * The generated typed data object. + */ + protected function getTypedDataFromContext(ContextInterface $context) { + // If the context has a value, just return that. + if ($context->hasContextValue()) { + return $context->getContextData(); + } + // Get the constraints from the context's definition. + $constraints = $this->getConstraintsFromContextDefinition($context->getContextDefinition()); + // If constraints include EntityType, we generate an entity or adapter. + if (!empty($constraints['EntityType']) && $constraints['EntityType'] instanceof EntityTypeConstraint) { + $entity_type_id = $constraints['EntityType']->type; + $storage = $this->entityTypeManager->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) { + $bundle = $constraints['Bundle']->bundle[0]; + // We have a bundle, we are bundleable and we can generate a sample. + return EntityAdapter::createFromEntity($storage->createWithSampleValues($bundle)); + } + } + // Either no bundle, or not bundleable, so generate an entity adapter. + $definition = EntityDataDefinition::create() + ->setEntityTypeId($entity_type_id); + return new EntityAdapter($definition); + } + // No entity related constraints, so generate a basic typed data object. + return $this->typedDataManager->createInstance($context->getContextDefinition()->getDataType()); + } + } diff --git a/core/lib/Drupal/Core/Plugin/Plugin/DataType/DataTypeWrapper.php b/core/lib/Drupal/Core/Plugin/Plugin/DataType/DataTypeWrapper.php deleted file mode 100644 index 4db11d0bbc..0000000000 --- a/core/lib/Drupal/Core/Plugin/Plugin/DataType/DataTypeWrapper.php +++ /dev/null @@ -1,26 +0,0 @@ -assertSame($expected, $this->contextHandler->checkRequirements($contexts, $requirements)); - } + public function testCheckRequirements() { + // No contexts, no requirements = TRUE + $this->assertSame(TRUE, $this->contextHandler->checkRequirements([], [])); - /** - * Provides data for testCheckRequirements(). - */ - public function providerTestCheckRequirements() { + // No contexts, any requirement = FALSE + $requirement_any = new ContextDefinition(); + $requirement_any->setRequired(TRUE); + $this->assertSame(FALSE, $this->contextHandler->checkRequirements([], [$requirement_any])); + + // No context, optional requirements = TRUE $requirement_optional = new ContextDefinition(); $requirement_optional->setRequired(FALSE); + $this->assertSame(TRUE, $this->contextHandler->checkRequirements([], [$requirement_optional])); - $requirement_any = new ContextDefinition(); - $requirement_any->setRequired(TRUE); + // No context, required or optional requirements = FALSE + $this->assertSame(FALSE, $this->contextHandler->checkRequirements([], [$requirement_any, $requirement_optional])); + // Any context, any requirement = TRUE $context_any = new Context(new ContextDefinition('any')); + $this->assertSame(TRUE, $this->contextHandler->checkRequirements([$context_any], [$requirement_any])); + // Article node context, page node requirement = FALSE $requirement_specific = new ContextDefinition('entity:node'); $requirement_specific->setConstraints(array('Bundle' => [0 => 'page'])); - $mismatch_def = new ContextDefinition('entity:node'); - $context_constraint_mismatch = new Context($mismatch_def->setConstraints(array('Bundle' => [0 => 'article']))); + $context_constraint_mismatch = new Context($mismatch_def->setConstraints(['EntityType' => 'node', 'Bundle' => [0 => 'article']])); + $this->assertSame(FALSE, $this->contextHandler->checkRequirements([$context_constraint_mismatch], [$requirement_specific])); + // User context, page node requirement = FALSE $context_datatype_mismatch = new Context(new ContextDefinition('entity:user')); + $this->assertSame(FALSE, $this->contextHandler->checkRequirements([$context_datatype_mismatch], [$requirement_specific])); + // Page node context, page node requirement = TRUE $specific_def = new ContextDefinition('entity:node'); $context_specific = new Context($specific_def->setConstraints(array('Bundle' => [0 => 'page']))); + $this->assertSame(TRUE, $this->contextHandler->checkRequirements([$context_specific], [$requirement_specific])); + // Page node context, page OR article node requirement = TRUE $requirement_complex = new ContextDefinition('entity:node'); $requirement_complex->setConstraints(['Bundle' => [0 => 'page', 1 => 'article']]); + $this->assertSame(TRUE, $this->contextHandler->checkRequirements([$context_specific], [$requirement_complex])); - $data = []; - $data[] = [[], [], TRUE]; - $data[] = [[], [$requirement_any], FALSE]; - $data[] = [[], [$requirement_optional], TRUE]; - $data[] = [[], [$requirement_any, $requirement_optional], FALSE]; - $data[] = [[$context_any], [$requirement_any], TRUE]; - $data[] = [[$context_constraint_mismatch], [$requirement_specific], FALSE]; - $data[] = [[$context_datatype_mismatch], [$requirement_specific], FALSE]; - $data[] = [[$context_specific], [$requirement_specific], TRUE]; - $data[] = array(array($context_specific), array($requirement_complex), TRUE); - $data[] = array(array($context_constraint_mismatch), array($requirement_complex), TRUE); - - return $data; + // Article node context, page OR article node requirement = TRUE + $this->assertSame(TRUE, $this->contextHandler->checkRequirements([$context_constraint_mismatch], [$requirement_complex])); } /** * @covers ::getMatchingContexts - * - * @dataProvider providerTestGetMatchingContexts - */ - public function testGetMatchingContexts($contexts, $requirement, $expected = NULL) { - if (is_null($expected)) { - $expected = $contexts; - } - $this->assertEquals($expected, $this->contextHandler->getMatchingContexts($contexts, $requirement)); - } - - /** - * Provides data for testGetMatchingContexts(). */ - public function providerTestGetMatchingContexts() { + public function testGetMatchingContexts() { $requirement_any = new ContextDefinition(); $requirement_specific = new ContextDefinition('entity:node'); @@ -132,39 +119,25 @@ public function providerTestGetMatchingContexts() { $context_definition_specific->setConstraints(array('Bundle' => [0 => 'page'])); $context_specific = new Context($context_definition_specific); - $data = []; // No context will return no valid contexts. - $data[] = [[], $requirement_any]; + $this->assertEquals([], $this->contextHandler->getMatchingContexts([], $requirement_any)); // A context with a generic matching requirement is valid. - $data[] = [[$context_any], $requirement_any]; + $this->assertEquals([$context_any], $this->contextHandler->getMatchingContexts([$context_any], $requirement_any)); // A context with a specific matching requirement is valid. - $data[] = [[$context_specific], $requirement_specific]; - + $this->assertEquals([$context_specific], $this->contextHandler->getMatchingContexts([$context_specific], $requirement_specific)); // A context with a mismatched constraint is invalid. - $data[] = [[$context_constraint_mismatch], $requirement_specific, []]; + $this->assertEquals([], $this->contextHandler->getMatchingContexts([$context_constraint_mismatch], $requirement_specific)); // A context with a mismatched datatype is invalid. - $data[] = [[$context_datatype_mismatch], $requirement_specific, []]; - - return $data; + $this->assertEquals([], $this->contextHandler->getMatchingContexts([$context_datatype_mismatch], $requirement_specific)); } /** * @covers ::filterPluginDefinitionsByContexts - * - * @dataProvider providerTestFilterPluginDefinitionsByContexts - */ - public function testFilterPluginDefinitionsByContexts($contexts, $definitions, $expected) { - $this->assertSame($expected, $this->contextHandler->filterPluginDefinitionsByContexts($contexts, $definitions)); - } - - /** - * Provides data for testFilterPluginDefinitionsByContexts(). */ - public function providerTestFilterPluginDefinitionsByContexts() { - $data = []; - + public function testFilterPluginDefinitionsByContexts() { $user = new ContextDefinition('entity:user'); $node = new ContextDefinition('entity:node'); + $string = new ContextDefinition('string'); $page = new ContextDefinition('entity:node'); $page->addConstraint('Bundle', [0 => 'page']); $article = new ContextDefinition('entity:node'); @@ -199,27 +172,33 @@ public function providerTestFilterPluginDefinitionsByContexts() { ], 'no_context_1' => [], 'no_context_2' => [], + 'needs_string' => [ + 'context' => [ + 'string' => $string, + ] + ] ]; // No context, plugins without contextual requirements available. - $data[] = [[], $plugins, ['no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']]]; + //$data[] = [[], $plugins, ['no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']]]; + $this->assertSame(['no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']], $this->contextHandler->filterPluginDefinitionsByContexts([], $plugins)); // Node context, plugins without contextual requirements and node specific // contexts available. - $data[] = [['node' => new Context($node)], $plugins, ['needs_node' => $plugins['needs_node'], 'needs_page' => $plugins['needs_page'], 'needs_article' => $plugins['needs_article'], 'needs_page_or_article' => $plugins['needs_page_or_article'], 'no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']]]; - // Node of bundle page contexts, plugins without contextual requirements and - // node or page specific bundle contexts available. - $data[] = [['page' => new Context($page)], $plugins, ['needs_node' => $plugins['needs_node'], 'needs_page' => $plugins['needs_page'], 'needs_page_or_article' => $plugins['needs_page_or_article'], 'no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']]]; + $this->assertSame(['needs_node' => $plugins['needs_node'], 'needs_page' => $plugins['needs_page'], 'needs_article' => $plugins['needs_article'], 'needs_page_or_article' => $plugins['needs_page_or_article'], 'no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']], $this->contextHandler->filterPluginDefinitionsByContexts(['node' => new Context($node)], $plugins)); // Node of bundle article contexts, plugins without contextual requirements // and node or article specific bundle contexts available. - $data[] = [['article' => new Context($article)], $plugins, ['needs_node' => $plugins['needs_node'], 'needs_article' => $plugins['needs_article'], 'needs_page_or_article' => $plugins['needs_page_or_article'], 'no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']]]; + $this->assertSame(['needs_node' => $plugins['needs_node'], 'needs_article' => $plugins['needs_article'], 'needs_page_or_article' => $plugins['needs_page_or_article'], 'no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']], $this->contextHandler->filterPluginDefinitionsByContexts(['node' => new Context($article)], $plugins)); + // Node of bundle page contexts, plugins without contextual requirements and + // node or page specific bundle contexts available. + $this->assertSame(['needs_node' => $plugins['needs_node'], 'needs_page' => $plugins['needs_page'], 'needs_page_or_article' => $plugins['needs_page_or_article'], 'no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']], $this->contextHandler->filterPluginDefinitionsByContexts(['node' => new Context($page)], $plugins)); // Nodes of bundles page and article contexts, plugins without contextual // requirements and node, page or article specific bundle contexts // available. - $data[] = [['page' => new Context($page), 'article' => new Context($article)], $plugins, ['needs_node' => $plugins['needs_node'], 'needs_page' => $plugins['needs_page'], 'needs_article' => $plugins['needs_article'], 'needs_page_or_article' => $plugins['needs_page_or_article'], 'no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']]]; + $this->assertSame(['needs_node' => $plugins['needs_node'], 'needs_page' => $plugins['needs_page'], 'needs_article' => $plugins['needs_article'], 'needs_page_or_article' => $plugins['needs_page_or_article'], 'no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']], $this->contextHandler->filterPluginDefinitionsByContexts(['page' => new Context($page), 'article' => new Context($article)], $plugins)); // User context, plugins without contextual requirements and user specific // contexts available. - $data[] = [['user' => new Context($user)], $plugins, ['needs_user' => $plugins['needs_user'], 'no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']]]; - - return $data; + $this->assertSame(['needs_user' => $plugins['needs_user'], 'no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2']], $this->contextHandler->filterPluginDefinitionsByContexts(['user' => new Context($user)], $plugins)); + // Non-entity contextual requirements test. + $this->assertSame(['no_context_1' => $plugins['no_context_1'], 'no_context_2' => $plugins['no_context_2'], 'needs_string' => $plugins['needs_string']], $this->contextHandler->filterPluginDefinitionsByContexts(['string' => new Context($string)], $plugins)); } /**