diff --git a/src/Plugin/Validation/Constraint/ValidDynamicReferenceConstraint.php b/src/Plugin/Validation/Constraint/ValidDynamicReferenceConstraint.php index f1960d2..0449c6b 100644 --- a/src/Plugin/Validation/Constraint/ValidDynamicReferenceConstraint.php +++ b/src/Plugin/Validation/Constraint/ValidDynamicReferenceConstraint.php @@ -43,8 +43,20 @@ class ValidDynamicReferenceConstraint extends Constraint { * @var string */ public $wrongIDMessage = 'The referenced entity (%type: %id) does not exist.'; + + /** + * Validation message when the entity type is not supported. + * + * @var string + */ public $wrongTypeMessage = 'The referenced entity type (%type) is not allowed for this field.'; - public $wrongBundleMessage = 'The entity bundle must be one of %bundles, the selected entity is of bundle %bundle.'; + + /** + * Validation message when bundle is not supported. + * + * @var string + */ + public $wrongBundleMessage = 'Referenced entity %label does not belong to one of the supported bundles (%bundles).'; /** * Validation message when the target_id or target_type is empty. diff --git a/src/Plugin/Validation/Constraint/ValidDynamicReferenceConstraintValidator.php b/src/Plugin/Validation/Constraint/ValidDynamicReferenceConstraintValidator.php index 4d83437..66cf329 100644 --- a/src/Plugin/Validation/Constraint/ValidDynamicReferenceConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ValidDynamicReferenceConstraintValidator.php @@ -63,92 +63,126 @@ class ValidDynamicReferenceConstraintValidator extends ConstraintValidator imple if (!isset($value)) { return; } - $field_definition_settings = $value->getFieldDefinition()->getSettings(); + + // Collect new entities, IDs and target types of existing entities across + // the field items. + $new_entities = []; + $target_ids = []; + $target_types = []; + $valid_target_types = DynamicEntityReferenceItem::getTargetTypes($value->getFieldDefinition()->getSettings()); foreach ($value as $delta => $item) { - $valid_target_type = DynamicEntityReferenceItem::getTargetTypes($item->getFieldDefinition() - ->getSettings()); - $target_id = $item->target_id; - $target_type = $item->target_type; - /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler */ - $handler = $this->selectionManager->getSelectionHandler($value->getFieldDefinition(), NULL, $target_type); // We don't use a regular NotNull constraint for the target_id property as // NULL is allowed if the entity property contains an unsaved entity. // @see \Drupal\Core\TypedData\DataReferenceTargetDefinition::getConstraints() - if (!$item->isEmpty() && ($target_id === NULL || $target_type === NULL)) { - $property = $target_id ? 'target_type' : 'target_id'; + if (!$item->isEmpty() && ($item->target_id === NULL || $item->target_type === NULL)) { + $property = $item->target_id ? 'target_type' : 'target_id'; if (!$item->entity->isNew()) { $this->context->buildViolation($constraint->nullMessage) ->setParameter('%property', $property) ->atPath((string) $delta . '.' . $property) ->addViolation(); - return; - } - // It is a new entity. - else { - $valid_new_entity = NULL; - if ($handler instanceof SelectionWithAutocreateInterface && $item->entity->getEntityTypeId() == $target_type) { - $valid_new_entity = $handler->validateReferenceableNewEntities([$item->entity]); - } - if (!$valid_new_entity) { - $this->context->buildViolation($constraint->invalidAutocreateMessage) - ->setParameter('%type', $item->entity->getEntityTypeId()) - ->setParameter('%label', $item->entity->label()) - ->atPath((string) $delta . '.entity') - ->setInvalidValue($item->entity) - ->addViolation(); - return; - } + continue; } + $new_entities[$delta] = $item->entity; } - $valid_type = empty($target_type) || (!empty($target_type) && in_array($target_type, $valid_target_type)); + + // Validate target type. + $valid_type = empty($item->target_type) || (!empty($item->target_type) && in_array($item->target_type, $valid_target_types)); if (!$valid_type) { + // Remove new entity if the target type is not valid. + if (isset($new_entities[$delta])) { + unset($new_entities[$delta]); + } $this->context->buildViolation($constraint->wrongTypeMessage) - ->setParameter('%type', $target_type) + ->setParameter('%type', $item->target_type) ->atPath((string) $delta . '.target_type') - ->setInvalidValue($target_type) + ->setInvalidValue($item->target_type) ->addViolation(); - return; + continue; + } + + // '0' or NULL are considered valid empty references. + if (!empty($item->target_id)) { + $target_ids[$delta] = $item->target_id; } + $target_types[$delta] = $item->target_type; + } + + // Collect all new and created valid entities fot bundle validation. + $valid_entities = []; - if ($target_id) { - $valid_target_id = $handler->validateReferenceableEntities([$target_id]); - if (!$valid_target_id) { - // For accuracy of the error message, differentiate non-referenceable - // and non-existent entities. - $definition = $this->entityTypeManager->getDefinition($target_type); - $existing_ids = $this->entityTypeManager->getStorage($target_type)->getQuery() - ->condition($definition->getKey('id'), $target_id, 'IN') - ->execute(); - $message = in_array($target_id, $existing_ids) ? $constraint->message : $constraint->nonExistingMessage; - $this->context->buildViolation($message) - ->setParameter('%type', $target_type) - ->setParameter('%id', $target_id) - ->atPath((string) $delta . '.target_id') - ->setInvalidValue($target_id) + // Validate new entities. + foreach ($new_entities as $delta => $new_entity) { + /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler */ + $handler = $this->selectionManager->getSelectionHandler($value->getFieldDefinition(), NULL, $target_types[$delta]); + if ($handler instanceof SelectionWithAutocreateInterface && ($new_entity->getEntityTypeId() == $target_types[$delta] || empty($target_types[$delta]))) { + if (!$handler->validateReferenceableNewEntities([$new_entity])) { + $this->context->buildViolation($constraint->invalidAutocreateMessage) + ->setParameter('%type', $new_entity->getEntityTypeId()) + ->setParameter('%label', $new_entity->label()) + ->atPath((string) $delta . '.entity') + ->setInvalidValue($new_entity) ->addViolation(); - return; + continue; } } + $valid_entities[$delta] = $new_entity; + } - $valid_bundle = FALSE; - $target_bundles = NULL; - if (array_key_exists('target_bundles', $field_definition_settings[$target_type]['handler_settings'])) { - $target_bundles = $field_definition_settings[$target_type]['handler_settings']['target_bundles']; - // NULL means all bundles are allowed. - if ($target_bundles == NULL || in_array($item->entity->bundle(), $target_bundles)) { + // Validate id for non existent entities. + foreach ($target_ids as $delta => $target_id) { + $definition = $this->entityTypeManager->getDefinition($target_types[$delta]); + $existing_id = $this->entityTypeManager->getStorage($target_types[$delta])->getQuery() + ->condition($definition->getKey('id'), $target_id, 'IN') + ->execute(); + if (!$existing_id) { + $this->context->buildViolation($constraint->nonExistingMessage) + ->setParameter('%type', $target_types[$delta]) + ->setParameter('%id', $target_id) + ->atPath((string) $delta . '.target_id') + ->setInvalidValue($target_id) + ->addViolation(); + unset($target_ids[$delta]); + continue; + } + $valid_entities[$delta] = $this->entityTypeManager->getStorage($target_types[$delta])->load($target_id); + } + + // Validate bundles. + foreach ($valid_entities as $delta => $entity) { + if (array_key_exists('target_bundles', $value->getFieldDefinition()->getSetting($target_types[$delta])['handler_settings'])) { + $valid_bundle = FALSE; + $target_bundles = $value->getFieldDefinition()->getSetting($target_types[$delta])['handler_settings']['target_bundles']; + if ($target_bundles === NULL || in_array($entity->bundle(), $target_bundles)) { $valid_bundle = TRUE; } if (!$valid_bundle) { $this->context->buildViolation($constraint->wrongBundleMessage) - ->setParameter('%bundles', $target_bundles) - ->setParameter('%bundle', $item->entity->bundle()) + ->setParameter('%label', $entity->label()) + ->setParameter('%bundles', implode(', ', $target_bundles)) ->atPath((string) $delta . '.entity') - ->setInvalidValue($item->entity) + ->setInvalidValue($entity) ->addViolation(); + if (isset($target_ids[$delta])) { + unset($target_ids[$delta]); + } } } } + // Validate target ids. + foreach ($target_ids as $delta => $id) { + $handler = $this->selectionManager->getSelectionHandler($value->getFieldDefinition(), NULL, $target_types[$delta]); + $valid_target_id = $handler->validateReferenceableEntities([$id]); + if (!$valid_target_id) { + $this->context->buildViolation($constraint->message) + ->setParameter('%type', $target_types[$delta]) + ->setParameter('%id', $id) + ->atPath((string) $delta . '.target_id') + ->setInvalidValue($id) + ->addViolation(); + } + } } } diff --git a/tests/src/Kernel/DynamicEntityReferenceFieldTest.php b/tests/src/Kernel/DynamicEntityReferenceFieldTest.php index 5aa6eaf..3139629 100644 --- a/tests/src/Kernel/DynamicEntityReferenceFieldTest.php +++ b/tests/src/Kernel/DynamicEntityReferenceFieldTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\dynamic_entity_reference\Kernel; use Drupal\config\Tests\SchemaCheckTestTrait; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\entity_test\Entity\EntityTestBundle; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; @@ -28,7 +29,7 @@ class DynamicEntityReferenceFieldTest extends EntityKernelTestBase { * * @var string */ - protected $referencedEntityType = 'entity_test_rev'; + protected $referencedEntityType = 'entity_test_with_bundle'; /** * The bundle used in this test. @@ -57,7 +58,7 @@ class DynamicEntityReferenceFieldTest extends EntityKernelTestBase { protected function setUp() { parent::setUp(); - $this->installEntitySchema('entity_test_rev'); + $this->installEntitySchema('entity_test_with_bundle'); // Create a field. FieldStorageConfig::create([ @@ -145,8 +146,58 @@ class DynamicEntityReferenceFieldTest extends EntityKernelTestBase { $this->assertEquals($violations->count(), 1, 'Validation throws a violation.'); $this->assertEquals($violations[0]->getMessage(), t('The referenced entity type (%type) is not allowed for this field.', ['%type' => $entity->getEntityTypeId()])); - // @todo Implement a test case for invalid bundle references after - // https://drupal.org/node/2064191 is fixed + // Test bundle validation with empty array. Empty array means no bundle is + // allowed. + $field_config = $this->container->get('entity_type.manager') + ->getStorage('field_config') + ->load($this->entityType . '.' . $this->bundle . '.' . $this->fieldName); + // Empty array means no target bundles are allowed. + $settings = [ + 'handler' => 'default:' . $this->referencedEntityType, + 'handler_settings' => [ + 'target_bundles' => [], + ], + ]; + $field_config->setSetting('entity_test_with_bundle', $settings); + $field_config->save(); + + $entity = $entity_type_manager + ->getStorage($this->entityType) + ->create(['type' => $this->bundle]); + $entity->{$this->fieldName}->target_type = $referenced_entity->getEntityTypeId(); + $entity->{$this->fieldName}->target_id = $referenced_entity->id(); + $violations = $entity->{$this->fieldName}->validate(); + $this->assertEquals($violations->count(), 1, 'Validation throws a violation.'); + $this->assertEquals($violations[0]->getMessage(), t('Referenced entity %label does not belong to one of the supported bundles (%bundles).', ['%label' => $referenced_entity->label(), '%bundles' => ''])); + + // Test with wrong bundle. + $bundle = EntityTestBundle::create([ + 'id' => 'newbundle', + 'label' => 'New Bundle', + 'revision' => FALSE, + ]); + $bundle->save(); + + $field_config = $this->container->get('entity_type.manager') + ->getStorage('field_config') + ->load($this->entityType . '.' . $this->bundle . '.' . $this->fieldName); + $settings = [ + 'handler' => 'default:' . $this->referencedEntityType, + 'handler_settings' => [ + 'target_bundles' => ['newbundle'], + ], + ]; + $field_config->setSetting('entity_test_with_bundle', $settings); + $field_config->save(); + + $entity = $entity_type_manager + ->getStorage($this->entityType) + ->create(['type' => $this->bundle]); + $entity->{$this->fieldName}->target_type = $referenced_entity->getEntityTypeId(); + $entity->{$this->fieldName}->target_id = $referenced_entity->id(); + $violations = $entity->{$this->fieldName}->validate(); + $this->assertEquals($violations->count(), 1, 'Validation throws a violation.'); + $this->assertEquals($violations[0]->getMessage(), t('Referenced entity %label does not belong to one of the supported bundles (%bundles).', ['%label' => $referenced_entity->label(), '%bundles' => 'newbundle'])); } /**