ValidReferenceConstraintValidator.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. <?php
  2. namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
  3. use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
  4. use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface;
  5. use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface;
  6. use Drupal\Core\Entity\EntityTypeManagerInterface;
  7. use Symfony\Component\DependencyInjection\ContainerInterface;
  8. use Symfony\Component\Validator\Constraint;
  9. use Symfony\Component\Validator\ConstraintValidator;
  10. /**
  11. * Checks if referenced entities are valid.
  12. */
  13. class ValidReferenceConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
  14. /**
  15. * The selection plugin manager.
  16. *
  17. * @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface
  18. */
  19. protected $selectionManager;
  20. /**
  21. * The entity type manager.
  22. *
  23. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  24. */
  25. protected $entityTypeManager;
  26. /**
  27. * Constructs a ValidReferenceConstraintValidator object.
  28. *
  29. * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager
  30. * The selection plugin manager.
  31. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
  32. * The entity type manager.
  33. */
  34. public function __construct(SelectionPluginManagerInterface $selection_manager, EntityTypeManagerInterface $entity_type_manager) {
  35. $this->selectionManager = $selection_manager;
  36. $this->entityTypeManager = $entity_type_manager;
  37. }
  38. /**
  39. * {@inheritdoc}
  40. */
  41. public static function create(ContainerInterface $container) {
  42. return new static(
  43. $container->get('plugin.manager.entity_reference_selection'),
  44. $container->get('entity_type.manager')
  45. );
  46. }
  47. /**
  48. * {@inheritdoc}
  49. */
  50. public function validate($value, Constraint $constraint) {
  51. /** @var \Drupal\Core\Field\FieldItemListInterface $value */
  52. /** @var ValidReferenceConstraint $constraint */
  53. if (!isset($value)) {
  54. return;
  55. }
  56. // Collect new entities and IDs of existing entities across the field items.
  57. $new_entities = [];
  58. $target_ids = [];
  59. foreach ($value as $delta => $item) {
  60. $target_id = $item->target_id;
  61. // We don't use a regular NotNull constraint for the target_id property as
  62. // NULL is allowed if the entity property contains an unsaved entity.
  63. // @see \Drupal\Core\TypedData\DataReferenceTargetDefinition::getConstraints()
  64. if (!$item->isEmpty() && $target_id === NULL) {
  65. if (!$item->entity->isNew()) {
  66. $this->context->buildViolation($constraint->nullMessage)
  67. ->atPath((string) $delta)
  68. ->addViolation();
  69. return;
  70. }
  71. $new_entities[$delta] = $item->entity;
  72. }
  73. // '0' or NULL are considered valid empty references.
  74. if (!empty($target_id)) {
  75. $target_ids[$delta] = $target_id;
  76. }
  77. }
  78. // Early opt-out if nothing to validate.
  79. if (!$new_entities && !$target_ids) {
  80. return;
  81. }
  82. $entity = !empty($value->getParent()) ? $value->getEntity() : NULL;
  83. /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler * */
  84. $handler = $this->selectionManager->getSelectionHandler($value->getFieldDefinition(), $entity);
  85. $target_type_id = $value->getFieldDefinition()->getSetting('target_type');
  86. // Add violations on deltas with a new entity that is not valid.
  87. if ($new_entities) {
  88. if ($handler instanceof SelectionWithAutocreateInterface) {
  89. $valid_new_entities = $handler->validateReferenceableNewEntities($new_entities);
  90. $invalid_new_entities = array_diff_key($new_entities, $valid_new_entities);
  91. }
  92. else {
  93. // If the selection handler does not support referencing newly created
  94. // entities, all of them should be invalidated.
  95. $invalid_new_entities = $new_entities;
  96. }
  97. foreach ($invalid_new_entities as $delta => $entity) {
  98. $this->context->buildViolation($constraint->invalidAutocreateMessage)
  99. ->setParameter('%type', $target_type_id)
  100. ->setParameter('%label', $entity->label())
  101. ->atPath((string) $delta . '.entity')
  102. ->setInvalidValue($entity)
  103. ->addViolation();
  104. }
  105. }
  106. // Add violations on deltas with a target_id that is not valid.
  107. if ($target_ids) {
  108. // Get a list of pre-existing references.
  109. $previously_referenced_ids = [];
  110. if ($value->getParent() && ($entity = $value->getEntity()) && !$entity->isNew()) {
  111. $existing_entity = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
  112. foreach ($existing_entity->{$value->getFieldDefinition()->getName()}->getValue() as $item) {
  113. $previously_referenced_ids[$item['target_id']] = $item['target_id'];
  114. }
  115. }
  116. $valid_target_ids = $handler->validateReferenceableEntities($target_ids);
  117. if ($invalid_target_ids = array_diff($target_ids, $valid_target_ids)) {
  118. // For accuracy of the error message, differentiate non-referenceable
  119. // and non-existent entities.
  120. $existing_entities = $this->entityTypeManager->getStorage($target_type_id)->loadMultiple($invalid_target_ids);
  121. foreach ($invalid_target_ids as $delta => $target_id) {
  122. // Check if any of the invalid existing references are simply not
  123. // accessible by the user, in which case they need to be excluded from
  124. // validation
  125. if (isset($previously_referenced_ids[$target_id]) && isset($existing_entities[$target_id]) && !$existing_entities[$target_id]->access('view')) {
  126. continue;
  127. }
  128. $message = isset($existing_entities[$target_id]) ? $constraint->message : $constraint->nonExistingMessage;
  129. $this->context->buildViolation($message)
  130. ->setParameter('%type', $target_type_id)
  131. ->setParameter('%id', $target_id)
  132. ->atPath((string) $delta . '.target_id')
  133. ->setInvalidValue($target_id)
  134. ->addViolation();
  135. }
  136. }
  137. }
  138. }
  139. }