diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 8d889a3928f94afc2a58d793ac67973bbe418d63..a9dd6478d17e050334f41fedadb39d6705ac7b3a 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -284,7 +284,8 @@ public function preSaveRevision(EntityStorageInterface $storage, \stdClass $reco * {@inheritdoc} */ public function validate() { - return $this->getTypedData()->validate(); + $violations = $this->getTypedData()->validate(); + return new EntityConstraintViolationList($this, iterator_to_array($violations)); } /** diff --git a/core/lib/Drupal/Core/Entity/ContentEntityForm.php b/core/lib/Drupal/Core/Entity/ContentEntityForm.php index 374060048839ef8573c208bf8f0ceec263123f34..2c1604bb9b8849e6583cf098be5d8de278c02118 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php @@ -75,8 +75,17 @@ public function form(array $form, FormStateInterface $form_state) { * https://www.drupal.org/node/2015613. */ public function validate(array $form, FormStateInterface $form_state) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $entity = $this->buildEntity($form, $form_state); - $this->getFormDisplay($form_state)->validateFormValues($entity, $form, $form_state); + + $violations = $entity->validate(); + + // Remove violations of inaccessible fields and not edited fields. + $violations + ->filterByFieldAccess($this->currentUser()) + ->filterByFields(array_diff(array_keys($entity->getFieldDefinitions()), $this->getEditedFieldNames($form_state))); + + $this->flagViolations($violations, $form, $form_state); // @todo Remove this. // Execute legacy global validation handlers. @@ -85,6 +94,54 @@ public function validate(array $form, FormStateInterface $form_state) { return $entity; } + /** + * Gets the names of all fields edited in the form. + * + * If the entity form customly adds some fields to the form (i.e. without + * using the form display), it needs to add its fields here and override + * flagViolations() for displaying the violations. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return string[] + * An array of field names. + */ + protected function getEditedFieldNames(FormStateInterface $form_state) { + return array_keys($this->getFormDisplay($form_state)->getComponents()); + } + + /** + * Flags violations for the current form. + * + * If the entity form customly adds some fields to the form (i.e. without + * using the form display), it needs to add its fields to array returned by + * getEditedFieldNames() and overwrite this method in order to show any + * violations for those fields; e.g.: + * @code + * foreach ($violations->getByField('name') as $violation) { + * $form_state->setErrorByName('name', $violation->getMessage()); + * } + * parent::flagViolations($violations, $form, $form_state); + * @endcode + * + * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations + * The violations to flag. + * @param array $form + * A nested array of form elements comprising the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) { + // Flag entity level violations. + foreach ($violations->getEntityViolations() as $violation) { + /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */ + $form_state->setErrorByName('', $violation->getMessage()); + } + // Let the form display flag violations of its fields. + $this->getFormDisplay($form_state)->flagWidgetsErrorsFromViolations($violations, $form, $form_state); + } + /** * Initializes the form state and the entity before the first form build. * diff --git a/core/lib/Drupal/Core/Entity/Display/EntityFormDisplayInterface.php b/core/lib/Drupal/Core/Entity/Display/EntityFormDisplayInterface.php index 57b5abf3f77a9662d5676fbe9d8d6117c3a949f8..b71b0ec2902a298fb71c99197062d47d82ddcb09 100644 --- a/core/lib/Drupal/Core/Entity/Display/EntityFormDisplayInterface.php +++ b/core/lib/Drupal/Core/Entity/Display/EntityFormDisplayInterface.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Entity\Display; +use Drupal\Core\Entity\EntityConstraintViolationListInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Form\FormStateInterface; @@ -100,11 +101,42 @@ interface EntityFormDisplayInterface extends EntityDisplayInterface { */ public function buildForm(FieldableEntityInterface $entity, array &$form, FormStateInterface $form_state); + /** + * Extracts field values from the submitted widget values into the entity. + * + * This accounts for drag-and-drop reordering of field values, and filtering + * of empty values. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity. + * @param array $form + * The form structure where field elements are attached to. This might be a + * full form structure, or a sub-element of a larger form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * An array whose keys and values are the keys of the top-level entries in + * $form_state->getValues() that have been processed. The remaining entries, + * if any, do not correspond to widgets and should be extracted manually by + * the caller if needed. + */ + public function extractFormValues(FieldableEntityInterface $entity, array &$form, FormStateInterface $form_state); + /** * Validates submitted widget values and sets the corresponding form errors. * - * There are two levels of validation for fields in forms: widget validation - * and field validation. + * This method invokes entity validation and takes care of flagging them on + * the form. This is particularly useful when all elements on the form are + * managed by the form display. + * + * As an alternative, entity validation can be invoked separately such that + * some violations can be flagged manually. In that case + * \Drupal\Core\Entity\Display\EntityFormDisplayInterface::flagViolations() + * must be used for flagging violations related to the form display. + * + * Note that there are two levels of validation for fields in forms: widget + * validation and field validation: * - Widget validation steps are specific to a given widget's own form * structure and UI metaphors. They are executed during normal form * validation, usually through Form API's #element_validate property. @@ -112,12 +144,9 @@ public function buildForm(FieldableEntityInterface $entity, array &$form, FormSt * extraction of proper field values from the submitted form input. * - If no form / widget errors were reported for the field, field validation * steps are performed according to the "constraints" specified by the - * field definition. Those are independent of the specific widget being - * used in a given form, and are also performed on REST entity submissions. - * - * This function performs field validation in the context of a form submission. - * It reports field constraint violations as form errors on the correct form - * elements. + * field definition as part of the entity validation. That validation is + * independent of the specific widget being used in a given form, and is + * also performed on REST entity submissions. * * @param \Drupal\Core\Entity\FieldableEntityInterface $entity * The entity. @@ -130,25 +159,26 @@ public function buildForm(FieldableEntityInterface $entity, array &$form, FormSt public function validateFormValues(FieldableEntityInterface $entity, array &$form, FormStateInterface $form_state); /** - * Extracts field values from the submitted widget values into the entity. + * Flags entity validation violations as form errors. * - * This accounts for drag-and-drop reordering of field values, and filtering - * of empty values. + * This method processes all violations passed, thus any violations not + * related to fields of the form display must be processed before this method + * is invoked. * - * @param \Drupal\Core\Entity\FieldableEntityInterface $entity - * The entity. + * The method flags constraint violations related to fields shown on the + * form as form errors on the correct form elements. Possibly pre-existing + * violations of hidden fields (so fields not appearing in the display) are + * ignored. Other, non-field related violations are passed through and set as + * form errors according to the property path of the violations. + * + * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations + * The violations to flag. * @param array $form * The form structure where field elements are attached to. This might be a * full form structure, or a sub-element of a larger form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. - * - * @return array - * An array whose keys and values are the keys of the top-level entries in - * $form_state->getValues() that have been processed. The remaining entries, - * if any, do not correspond to widgets and should be extracted manually by - * the caller if needed. */ - public function extractFormValues(FieldableEntityInterface $entity, array &$form, FormStateInterface $form_state); + public function flagWidgetsErrorsFromViolations(EntityConstraintViolationListInterface $violations, array &$form, FormStateInterface $form_state); } diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php index d0280a046276b871bbacc05ce9c9e064a28fd742..b020891f724b1413bdcb6305e58c3f2bd0df9f53 100644 --- a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php +++ b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php @@ -7,11 +7,15 @@ namespace Drupal\Core\Entity\Entity; +use Drupal\Core\Entity\EntityConstraintViolationListInterface; use Drupal\Core\Entity\EntityDisplayPluginCollection; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\Display\EntityFormDisplayInterface; use Drupal\Core\Entity\EntityDisplayBase; use Drupal\Core\Form\FormStateInterface; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\ConstraintViolationListInterface; /** * Configuration entity that contains widget options for all components of a @@ -231,18 +235,88 @@ public function extractFormValues(FieldableEntityInterface $entity, array &$form * {@inheritdoc} */ public function validateFormValues(FieldableEntityInterface $entity, array &$form, FormStateInterface $form_state) { - foreach ($entity as $field_name => $items) { - // Only validate the fields that actually appear in the form, and let the - // widget assign the violations to the right form elements. + $violations = $entity->validate(); + $violations->filterByFieldAccess(); + + // Flag entity level violations. + foreach ($violations->getEntityViolations() as $violation) { + /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */ + $form_state->setErrorByName('', $violation->getMessage()); + } + + $this->flagWidgetsErrorsFromViolations($violations, $form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function flagWidgetsErrorsFromViolations(EntityConstraintViolationListInterface $violations, array &$form, FormStateInterface $form_state) { + $entity = $violations->getEntity(); + foreach ($violations->getFieldNames() as $field_name) { + // Only show violations for fields that actually appear in the form, and + // let the widget assign the violations to the correct form elements. if ($widget = $this->getRenderer($field_name)) { - $violations = $items->validate(); - if (count($violations)) { - $widget->flagErrors($items, $violations, $form, $form_state); - } + $field_violations = $this->movePropertyPathViolationsRelativeToField($field_name, $violations->getByField($field_name)); + $widget->flagErrors($entity->get($field_name), $field_violations, $form, $form_state); } } } + /** + * Moves the property path to be relative to field level. + * + * @param string $field_name + * The field name. + * @param \Symfony\Component\Validator\ConstraintViolationListInterface $violations + * The violations. + * + * @return \Symfony\Component\Validator\ConstraintViolationList + * A new constraint violation list with the changed property path. + */ + protected function movePropertyPathViolationsRelativeToField($field_name, ConstraintViolationListInterface $violations) { + $new_violations = new ConstraintViolationList(); + foreach ($violations as $violation) { + // All the logic below is necessary to change the property path of the + // violations to be relative to the item list, so like title.0.value gets + // changed to 0.value. Sadly constraints in Symfony don't have setters so + // we have to create new objects. + /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */ + // Create a new violation object with just a different property path. + $violation_path = $violation->getPropertyPath(); + $path_parts = explode('.', $violation_path); + if ($path_parts[0] === $field_name) { + unset($path_parts[0]); + } + $new_path = implode('.', $path_parts); + + $constraint = NULL; + $cause = NULL; + $parameters = []; + $plural = NULL; + if ($violation instanceof ConstraintViolation) { + $constraint = $violation->getConstraint(); + $cause = $violation->getCause(); + $parameters = $violation->getParameters(); + $plural = $violation->getPlural(); + } + + $new_violation = new ConstraintViolation( + $violation->getMessage(), + $violation->getMessageTemplate(), + $parameters, + $violation->getRoot(), + $new_path, + $violation->getInvalidValue(), + $plural, + $violation->getCode(), + $constraint, + $cause + ); + $new_violations->add($new_violation); + } + return $new_violations; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/EntityConstraintViolationList.php b/core/lib/Drupal/Core/Entity/EntityConstraintViolationList.php new file mode 100644 index 0000000000000000000000000000000000000000..507975cd3db356f3d558171b4b6ed8aefd0606c2 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityConstraintViolationList.php @@ -0,0 +1,215 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityConstraintViolationList. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\Validator\ConstraintViolationList; + +/** + * Implements an entity constraint violation list. + */ +class EntityConstraintViolationList extends ConstraintViolationList implements EntityConstraintViolationListInterface { + + use StringTranslationTrait; + + /** + * The entity that has been validated. + * + * @var \Drupal\Core\Entity\FieldableEntityInterface + */ + protected $entity; + + /** + * Violations offsets of entity level violations. + * + * @var int[]|null + */ + protected $entityViolationOffsets; + + /** + * Violation offsets grouped by field. + * + * Keys are field names, values are arrays of violation offsets. + * + * @var array[]|null + */ + protected $violationOffsetsByField; + + /** + * {@inheritdoc} + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity that has been validated. + * @param array $violations + * The array of violations. + */ + public function __construct(FieldableEntityInterface $entity, array $violations = array()) { + parent::__construct($violations); + $this->entity = $entity; + } + + /** + * Groups violation offsets by field and entity level. + * + * Sets the $violationOffsetsByField and $entityViolationOffsets properties. + */ + protected function groupViolationOffsets() { + if (!isset($this->violationOffsetsByField)) { + $this->violationOffsetsByField = []; + $this->entityViolationOffsets = []; + foreach ($this as $offset => $violation) { + if ($path = $violation->getPropertyPath()) { + // An example of $path might be 'title.0.value'. + list($field_name) = explode('.', $path, 2); + if ($this->entity->hasField($field_name)) { + $this->violationOffsetsByField[$field_name][$offset] = $offset; + } + } + else { + $this->entityViolationOffsets[$offset] = $offset; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function getEntityViolations() { + $this->groupViolationOffsets(); + $violations = []; + foreach ($this->entityViolationOffsets as $offset) { + $violations[] = $this->get($offset); + } + return new static($this->entity, $violations); + } + + /** + * {@inheritdoc} + */ + public function getByField($field_name) { + return $this->getByFields([$field_name]); + } + + /** + * {@inheritdoc} + */ + public function getByFields(array $field_names) { + $this->groupViolationOffsets(); + $violations = []; + foreach (array_intersect_key($this->violationOffsetsByField, array_flip($field_names)) as $field_name => $offsets) { + foreach ($offsets as $offset) { + $violations[] = $this->get($offset); + } + } + return new static($this->entity, $violations); + } + + /** + * {@inheritdoc} + */ + public function filterByFields(array $field_names) { + $this->groupViolationOffsets(); + $new_violations = []; + foreach (array_intersect_key($this->violationOffsetsByField, array_flip($field_names)) as $field_name => $offsets) { + foreach ($offsets as $offset) { + $violation = $this->get($offset); + // Take care of composite field violations and re-map them to some + // covered field if necessary. + if ($violation->getConstraint() instanceof CompositeConstraintBase) { + $covered_fields = $violation->getConstraint()->coversFields(); + + // Keep the composite field if it covers some remaining field and put + // a violation on some other covered field instead. + if ($remaining_fields = array_diff($covered_fields, $field_names)) { + $message_params = ['%field_name' => $field_name]; + $violation = new ConstraintViolation( + $this->t('The validation failed because the value conflicts with the value in %field_name, which you cannot access.', $message_params), + 'The validation failed because the value conflicts with the value in %field_name, which you cannot access.', + $message_params, + $violation->getRoot(), + reset($remaining_fields), + $violation->getInvalidValue(), + $violation->getPlural(), + $violation->getCode(), + $violation->getConstraint(), + $violation->getCause() + ); + $new_violations[] = $violation; + } + } + + $this->remove($offset); + } + } + foreach ($new_violations as $violation) { + $this->add($violation); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function filterByFieldAccess(AccountInterface $account = NULL) { + $filtered_fields = array(); + foreach ($this->getFieldNames() as $field_name) { + if (!$this->entity->get($field_name)->access('edit', $account)) { + $filtered_fields[] = $field_name; + } + } + return $this->filterByFields($filtered_fields); + } + + /** + * {@inheritdoc} + */ + public function getFieldNames() { + $this->groupViolationOffsets(); + return array_keys($this->violationOffsetsByField); + } + + /** + * {@inheritdoc} + */ + public function getEntity() { + return $this->entity; + } + + /** + * {@inheritdoc} + */ + public function add(ConstraintViolationInterface $violation) { + parent::add($violation); + $this->violationOffsetsByField = NULL; + $this->entityViolationOffsets = NULL; + } + + /** + * {@inheritdoc} + */ + public function remove($offset) { + parent::remove($offset); + $this->violationOffsetsByField = NULL; + $this->entityViolationOffsets = NULL; + } + + /** + * {@inheritdoc} + */ + public function set($offset, ConstraintViolationInterface $violation) { + parent::set($offset, $violation); + $this->violationOffsetsByField = NULL; + $this->entityViolationOffsets = NULL; + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityConstraintViolationListInterface.php b/core/lib/Drupal/Core/Entity/EntityConstraintViolationListInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..423179044f9e9a6007ebcab16b65186284cf10fb --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityConstraintViolationListInterface.php @@ -0,0 +1,100 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityConstraintViolationListInterface. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Core\Session\AccountInterface; +use Symfony\Component\Validator\ConstraintViolationListInterface; + +/** + * Interface for the result of entity validation. + * + * The Symfony violation list is extended with methods that allow filtering + * violations by fields and field access. Forms leverage that to skip possibly + * pre-existing violations that cannot be caused or fixed by the form. + */ +interface EntityConstraintViolationListInterface extends ConstraintViolationListInterface { + + /** + * Gets violations flagged on entity level, not associated with any field. + * + * @return \Drupal\Core\Entity\EntityConstraintViolationListInterface + * A list of violations on the entity level. + */ + public function getEntityViolations(); + + /** + * Gets the violations of the given field. + * + * @param string $field_name + * The name of the field to get violations for. + * + * @return \Symfony\Component\Validator\ConstraintViolationListInterface + * The violations of the given field. + */ + public function getByField($field_name); + + /** + * Gets the violations of the given fields. + * + * When violations should be displayed for a sub-set of visible fields only, + * this method may be used to filter the set of visible violations first. + * + * @param string[] $field_names + * The names of the fields to get violations for. + * + * @return \Drupal\Core\Entity\EntityConstraintViolationListInterface + * A list of violations of the given fields. + */ + public function getByFields(array $field_names); + + /** + * Filters this violation list by the given fields. + * + * The returned object just has violations attached to the provided fields. + * + * When violations should be displayed for a sub-set of visible fields only, + * this method may be used to filter the set of visible violations first. + * + * @param string[] $field_names + * The names of the fields to filter violations for. + * + * @return $this + */ + public function filterByFields(array $field_names); + + /** + * Filters this violation list to apply for accessible fields only. + * + * Violations for inaccessible fields are removed so the returned object just + * has the remaining violations. + * + * @param \Drupal\Core\Session\AccountInterface $account + * (optional) The user for which to check access, or NULL to check access + * for the current user. Defaults to NULL. + * + * @return $this + */ + public function filterByFieldAccess(AccountInterface $account = NULL); + + /** + * Returns the names of all violated fields. + * + * @return string[] + * An array of field names. + */ + public function getFieldNames(); + + /** + * The entity which has been validated. + * + * @return \Drupal\Core\Entity\FieldableEntityInterface + * The entity object. + */ + public function getEntity(); + +} diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php index 813a91039d7f57c11de00c54b67785ec5d0ba359..b4a19602671df1a7b494059ab5ab9f1635454be5 100644 --- a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php +++ b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php @@ -206,7 +206,7 @@ public function onChange($field_name); /** * Validates the currently set values. * - * @return \Symfony\Component\Validator\ConstraintViolationListInterface + * @return \Drupal\Core\Entity\EntityConstraintViolationListInterface * A list of constraint violations. If the list is empty, validation * succeeded. */ diff --git a/core/modules/comment/src/CommentForm.php b/core/modules/comment/src/CommentForm.php index 12f5bb8f63d09c4dff9465016b5c1dafd5ac3ff4..40bfbf31bd0cb448f256e0cf48ec797ff4fb6166 100644 --- a/core/modules/comment/src/CommentForm.php +++ b/core/modules/comment/src/CommentForm.php @@ -13,6 +13,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\ContentEntityForm; +use Drupal\Core\Entity\EntityConstraintViolationListInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; @@ -314,24 +315,22 @@ public function buildEntity(array $form, FormStateInterface $form_state) { /** * {@inheritdoc} */ - public function validate(array $form, FormStateInterface $form_state) { - $comment = parent::validate($form, $form_state); + protected function getEditedFieldNames(FormStateInterface $form_state) { + return array_merge(['created', 'name'], parent::getEditedFieldNames($form_state)); + } - // Customly trigger validation of manually added fields and add in - // violations. - $violations = $comment->created->validate(); - foreach ($violations as $violation) { + /** + * {@inheritdoc} + */ + protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) { + // Manually flag violations of fields not handled by the form display. + foreach ($violations->getByField('created') as $violation) { $form_state->setErrorByName('date', $violation->getMessage()); } - $violations = $comment->validate(); - // Filter out violations for the name path. - foreach ($violations as $violation) { - if ($violation->getPropertyPath() === 'name') { - $form_state->setErrorByName('name', $violation->getMessage()); - } + foreach ($violations->getByField('name') as $violation) { + $form_state->setErrorByName('name', $violation->getMessage()); } - - return $comment; + parent::flagViolations($violations, $form, $form_state); } /** diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php index 76e05bd46e1c2df6c7522e89a8f9de37ba959195..788786a93c08eb0d3afc83a3f0796329a395333e 100644 --- a/core/modules/content_translation/src/ContentTranslationHandler.php +++ b/core/modules/content_translation/src/ContentTranslationHandler.php @@ -135,7 +135,6 @@ public function getFieldDefinitions() { $definitions['content_translation_changed'] = BaseFieldDefinition::create('changed') ->setLabel(t('Translation changed time')) ->setDescription(t('The Unix timestamp when the translation was most recently saved.')) - ->setPropertyConstraints('value', array('EntityChanged' => array())) ->setRevisionable(TRUE) ->setTranslatable(TRUE); } diff --git a/core/modules/node/node.module b/core/modules/node/node.module index aa26ce4bcc3e6086d21a84a302ae59db9e362293..a448b6c6a7af9e85a8c1702d3be9292908cbf140 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -737,28 +737,6 @@ function node_page_title(NodeInterface $node) { return $node->label(); } -/** - * Finds the last time a node was changed. - * - * @param $nid - * The ID of a node. - * @param string $langcode - * (optional) The language to get the last changed time for. If omitted, the - * last changed time across all translations will be returned. - * - * @return string - * A unix timestamp indicating the last time the node was changed. - * - * @todo Remove once https://www.drupal.org/node/2002180 is resolved. It's only - * used for validation, which will be done by - * EntityChangedConstraintValidator. - */ -function node_last_changed($nid, $langcode = NULL) { - $node = \Drupal::entityManager()->getStorage('node')->loadUnchanged($nid); - $changed = $langcode ? $node->getTranslation($langcode)->getChangedTime() : $node->getChangedTimeAcrossTranslations(); - return $changed ?: FALSE; -} - /** * Finds the most recently changed nodes that are available to the current user. * diff --git a/core/modules/node/src/NodeForm.php b/core/modules/node/src/NodeForm.php index d88393dc3c01a1fe37ec78c020a2ff6c7ccec75c..c57baa94f07b3e1a3651e67469f82ac41ee4f85c 100644 --- a/core/modules/node/src/NodeForm.php +++ b/core/modules/node/src/NodeForm.php @@ -283,19 +283,6 @@ protected function actions(array $form, FormStateInterface $form_state) { return $element; } - /** - * {@inheritdoc} - */ - public function validate(array $form, FormStateInterface $form_state) { - $node = parent::validate($form, $form_state); - - if ($node->id() && (node_last_changed($node->id()) > $node->getChangedTimeAcrossTranslations())) { - $form_state->setErrorByName('changed', $this->t('The content on this page has either been modified by another user, or you have already submitted modifications using this form. As a result, your changes cannot be saved.')); - } - - return $node; - } - /** * {@inheritdoc} * diff --git a/core/modules/node/src/Tests/NodeLastChangedTest.php b/core/modules/node/src/Tests/NodeLastChangedTest.php deleted file mode 100644 index 23fa4b424a38aec69788b17d00b918c025423503..0000000000000000000000000000000000000000 --- a/core/modules/node/src/Tests/NodeLastChangedTest.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\node\Tests\NodeLastChangedTest. - */ - -namespace Drupal\node\Tests; - -use Drupal\simpletest\KernelTestBase; - -/** - * Tests the node_last_changed() function. - * - * @group node - */ -class NodeLastChangedTest extends KernelTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array('user', 'node', 'field', 'system', 'text', 'filter'); - - protected function setUp() { - parent::setUp(); - $this->installEntitySchema('node'); - $this->installEntitySchema('user'); - } - - /** - * Runs basic tests for node_last_changed function. - */ - function testNodeLastChanged() { - $node = entity_create('node', array('type' => 'article', 'title' => $this->randomMachineName())); - $node->save(); - - // Test node last changed timestamp. - $changed_timestamp = node_last_changed($node->id()); - $this->assertEqual($changed_timestamp, $node->getChangedTimeAcrossTranslations(), 'Expected last changed timestamp returned.'); - - $changed_timestamp = node_last_changed($node->id(), $node->language()->getId()); - $this->assertEqual($changed_timestamp, $node->getChangedTime(), 'Expected last changed timestamp returned.'); - } -} diff --git a/core/modules/quickedit/src/Form/QuickEditFieldForm.php b/core/modules/quickedit/src/Form/QuickEditFieldForm.php index da262d47945a89dc8004ca1e6bfd0e13022df1ed..69a8442cb83b3dc626999018b45097867ef9829b 100644 --- a/core/modules/quickedit/src/Form/QuickEditFieldForm.php +++ b/core/modules/quickedit/src/Form/QuickEditFieldForm.php @@ -156,20 +156,7 @@ protected function init(FormStateInterface $form_state, EntityInterface $entity, */ public function validateForm(array &$form, FormStateInterface $form_state) { $entity = $this->buildEntity($form, $form_state); - $form_state->get('form_display')->validateFormValues($entity, $form, $form_state); - - // Run entity-level validation as well, while skipping validation of all - // fields. We can do so by fetching and validating the entity-level - // constraints manually. - // @todo: Improve this in https://www.drupal.org/node/2395831. - $typed_entity = $entity->getTypedData(); - $violations = $this->validator - ->validate($typed_entity, $typed_entity->getConstraints()); - - foreach ($violations as $violation) { - $form_state->setErrorByName($violation->getPropertyPath(), $violation->getMessage()); - } } /** diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index f7a0fa440a0f8a47d82e06a5df5af70e4d5e8ebc..7ed3ff6d5cbf6724c0ef3902e54ee2058747e13e 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -209,6 +209,11 @@ public function delete(EntityInterface $entity) { */ protected function validate(EntityInterface $entity) { $violations = $entity->validate(); + + // Remove violations of inaccessible fields as they cannot stem from our + // changes. + $violations->filterByFieldAccess(); + if (count($violations) > 0) { $message = "Unprocessable Entity: validation failed.\n"; foreach ($violations as $violation) { diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 721fdd91acb39f72ebfe69751b5e6ce8bd9d242a..a5dd73ece6e19c5c2a258d1babc7dd93ff2bf3f3 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -1590,7 +1590,7 @@ protected function drupalPostForm($path, $edit, $submit, array $options = array( if (!$ajax && isset($submit)) { $this->assertTrue($submit_matches, format_string('Found the @submit button', array('@submit' => $submit))); } - $this->fail(format_string('Found the requested form fields at @path', array('@path' => $path))); + $this->fail(format_string('Found the requested form fields at @path', array('@path' => ($path instanceof Url) ? $path->toString() : $path))); } } diff --git a/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php b/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php index 0ed3ec7086f5e629a0aded375c60751486e3f86a..5f369734f24f62a9534cb97b3251e1eb29416671 100644 --- a/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php +++ b/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php @@ -7,7 +7,10 @@ namespace Drupal\system\Tests\Entity; +use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormState; +use Drupal\entity_test\Entity\EntityTestCompositeConstraint; use Drupal\simpletest\KernelTestBase; use Drupal\system\Tests\TypedData; @@ -30,6 +33,7 @@ protected function setUp() { $this->container->get('router.builder')->rebuild(); $this->installEntitySchema('user'); + $this->installEntitySchema('entity_test_composite_constraint'); } /** @@ -57,4 +61,94 @@ public function testValidation() { $this->assertEqual($errors['name'], 'Widget constraint has failed.', 'Constraint violation is generated correctly'); } + /** + * Gets the form errors for a given entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity + * @param array $hidden_fields + * (optional) A list of hidden fields. + * + * @return array + * The form errors. + */ + protected function getErrorsForEntity(EntityInterface $entity, $hidden_fields = []) { + $entity_type_id = 'entity_test_composite_constraint'; + $display = entity_get_form_display($entity_type_id, $entity_type_id, 'default'); + + foreach ($hidden_fields as $hidden_field) { + $display->removeComponent($hidden_field); + } + + $form = []; + $form_state = new FormState(); + $display->buildForm($entity, $form, $form_state); + + $form_state->setFormObject(\Drupal::entityManager()->getFormObject($entity_type_id, 'default')); + \Drupal::formBuilder()->prepareForm('field_test_entity_form', $form, $form_state); + \Drupal::formBuilder()->processForm('field_test_entity_form', $form, $form_state); + + // Validate the field constraint. + $form_state->getFormObject()->setEntity($entity)->setFormDisplay($display, $form_state); + $form_state->getFormObject()->validate($form, $form_state); + + return $form_state->getErrors(); + } + + /** + * Tests widget constraint validation with composite constraints. + */ + public function testValidationWithCompositeConstraint() { + // First provide a valid value, this should cause no validation. + $entity = EntityTestCompositeConstraint::create([ + 'name' => 'valid-value', + ]); + $entity->save(); + + $errors = $this->getErrorsForEntity($entity); + $this->assertFalse(isset($errors['name'])); + $this->assertFalse(isset($errors['type'])); + + // Provide an invalid value for the name field. + $entity = EntityTestCompositeConstraint::create([ + 'name' => 'failure-field-name', + ]); + $errors = $this->getErrorsForEntity($entity); + $this->assertTrue(isset($errors['name'])); + $this->assertFalse(isset($errors['type'])); + + // Hide the second field (type) and ensure the validation still happens. The + // error message appears on the first field (name). + $entity = EntityTestCompositeConstraint::create([ + 'name' => 'failure-field-name', + ]); + $errors = $this->getErrorsForEntity($entity, ['type']); + $this->assertTrue(isset($errors['name'])); + $this->assertFalse(isset($errors['type'])); + + // Provide a violation again, but this time hide the first field (name). + // Ensure that the validation still happens and the error message is moved + // from the field to the second field and have a custom error message. + $entity = EntityTestCompositeConstraint::create([ + 'name' => 'failure-field-name', + ]); + $errors = $this->getErrorsForEntity($entity, ['name']); + $this->assertFalse(isset($errors['name'])); + $this->assertTrue(isset($errors['type'])); + $this->assertEqual($errors['type'], SafeMarkup::format('The validation failed because the value conflicts with the value in %field_name, which you cannot access.', ['%field_name' => 'name'])); + } + + /** + * Tests entity level constraint validation. + */ + public function testEntityLevelConstraintValidation() { + $entity = EntityTestCompositeConstraint::create([ + 'name' => 'entity-level-violation' + ]); + $entity->save(); + + $errors = $this->getErrorsForEntity($entity); + $this->assertEqual($errors[''], 'Entity level validation'); + } + } diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestCompositeConstraint.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestCompositeConstraint.php index c387b939c5dd677abe83be851d6dc96df73fced1..e092c1c5c39f6fce92031db23a387b5391783fab 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestCompositeConstraint.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestCompositeConstraint.php @@ -6,6 +6,7 @@ */ namespace Drupal\entity_test\Entity; +use Drupal\Core\Entity\EntityTypeInterface; /** * Defines a test class for testing composite constraints. @@ -19,13 +20,38 @@ * "bundle" = "type", * "label" = "name" * }, + * handlers = { + * "form" = { + * "default" = "Drupal\entity_test\EntityTestForm" + * } + * }, * base_table = "entity_test_composite_constraint", * persistent_cache = FALSE, * constraints = { * "EntityTestComposite" = {}, + * "EntityTestEntityLevel" = {}, * } * ) */ class EntityTestCompositeConstraint extends EntityTest { + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields = parent::baseFieldDefinitions($entity_type); + + $fields['name']->setDisplayOptions('form', array( + 'type' => 'string', + 'weight' => 0, + )); + + $fields['type']->setDisplayOptions('form', array( + 'type' => 'entity_reference_autocomplete', + 'weight' => 0, + )); + + return $fields; + } + } diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/EntityTestCompositeConstraintValidator.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/EntityTestCompositeConstraintValidator.php index 54fb969590826f5afc9893f42c17813b9561b59b..d643b7499984c39c915e3f4e46f1c57adc3d5204 100644 --- a/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/EntityTestCompositeConstraintValidator.php +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/EntityTestCompositeConstraintValidator.php @@ -7,6 +7,7 @@ namespace Drupal\entity_test\Plugin\Validation\Constraint; +use Drupal\Core\Entity\EntityTypeInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -32,7 +33,16 @@ public function validate($entity, Constraint $constraint) { ->atPath('type') ->addViolation(); } - + if ($entity->name->value === 'failure-field-name') { + $this->context->buildViolation('Name field violation') + ->atPath('name') + ->addViolation(); + } + elseif ($entity->name->value === 'failure-field-type') { + $this->context->buildViolation('Type field violation') + ->atPath('type') + ->addViolation(); + } } } diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/EntityTestEntityLevel.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/EntityTestEntityLevel.php new file mode 100644 index 0000000000000000000000000000000000000000..ec647bd7acc7b0a5d319e476391b10cf1def9679 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/EntityTestEntityLevel.php @@ -0,0 +1,25 @@ +<?php + +/** + * @file + * Contains \Drupal\entity_test\Plugin\Validation\Constraint\EntityTestEntityLevel. + */ + +namespace Drupal\entity_test\Plugin\Validation\Constraint; + +use Symfony\Component\Validator\Constraint; + +/** + * Constraint on entity entity level. + * + * @Plugin( + * id = "EntityTestEntityLevel", + * label = @Translation("Constraint on the entity level."), + * type = "entity" + * ) + */ +class EntityTestEntityLevel extends Constraint { + + public $message = 'Entity level validation'; + +} diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/EntityTestEntityLevelValidator.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/EntityTestEntityLevelValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..78d878528fb6688ec52345a062eb9303b1f9347b --- /dev/null +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/EntityTestEntityLevelValidator.php @@ -0,0 +1,35 @@ +<?php + +/** + * @file + * Contains \Drupal\entity_test\Plugin\Validation\Constraint\EntityTestEntityLevelValidator. + */ + +namespace Drupal\entity_test\Plugin\Validation\Constraint; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; + +/** + * Constraint validator for the EntityTestEntityLevel constraint. + */ +class EntityTestEntityLevelValidator extends ConstraintValidator { + + /** + * Validator 2.5 and upwards compatible execution context. + * + * @var \Symfony\Component\Validator\Context\ExecutionContextInterface + */ + protected $context; + + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) { + if ($value->name->value === 'entity-level-violation') { + $this->context->buildViolation($constraint->message) + ->addViolation(); + } + } + +} diff --git a/core/modules/user/src/AccountForm.php b/core/modules/user/src/AccountForm.php index f7345744412a7343d331e73df63ff2e846c028a1..0bcfad937b26579a97ceea04f3b1181a4a00bafa 100644 --- a/core/modules/user/src/AccountForm.php +++ b/core/modules/user/src/AccountForm.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Entity\ContentEntityForm; +use Drupal\Core\Entity\EntityConstraintViolationListInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Form\FormStateInterface; @@ -348,23 +349,35 @@ public function buildEntity(array $form, FormStateInterface $form_state) { $account->setExistingPassword($current_pass); } + // Skip the protected user field constraint if the user came from the + // password recovery page. + $account->_skipProtectedUserFieldConstraint = $form_state->get('user_pass_reset'); + return $account; } /** * {@inheritdoc} */ - public function validate(array $form, FormStateInterface $form_state) { - /** @var \Drupal\user\UserInterface $account */ - $account = parent::validate($form, $form_state); - - // Skip the protected user field constraint if the user came from the - // password recovery page. - $account->_skipProtectedUserFieldConstraint = $form_state->get('user_pass_reset'); + protected function getEditedFieldNames(FormStateInterface $form_state) { + return array_merge(array( + 'name', + 'pass', + 'mail', + 'timezone', + 'langcode', + 'preferred_langcode', + 'preferred_admin_langcode' + ), parent::getEditedFieldNames($form_state)); + } - // Customly trigger validation of manually added fields and add in - // violations. This is necessary as entity form displays only invoke entity - // validation for fields contained in the display. + /** + * {@inheritdoc} + */ + protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) { + // Manually flag violations of fields not handled by the form display. This + // is necessary as entity form displays only flag violations for fields + // contained in the display. $field_names = array( 'name', 'pass', @@ -374,14 +387,11 @@ public function validate(array $form, FormStateInterface $form_state) { 'preferred_langcode', 'preferred_admin_langcode' ); - foreach ($field_names as $field_name) { - $violations = $account->$field_name->validate(); - foreach ($violations as $violation) { - $form_state->setErrorByName($field_name, $violation->getMessage()); - } + foreach ($violations->getByFields($field_names) as $violation) { + list($field_name) = explode('.', $violation->getPropertyPath(), 2); + $form_state->setErrorByName($field_name, $violation->getMessage()); } - - return $account; + parent::flagViolations($violations, $form, $form_state); } /** diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityConstraintViolationListTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityConstraintViolationListTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7126d534bd670355cfba2698720491311ffbcc12 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/EntityConstraintViolationListTest.php @@ -0,0 +1,168 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Entity\EntityConstraintViolationListTest. + */ + +namespace Drupal\Tests\Core\Entity; + +use Drupal\Core\Entity\EntityConstraintViolationList; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\entity_test\Plugin\Validation\Constraint\EntityTestCompositeConstraint; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\Validator\ConstraintViolation; + +/** + * @coversDefaultClass \Drupal\Core\Entity\EntityConstraintViolationList + * @group entity + */ +class EntityConstraintViolationListTest extends UnitTestCase { + + /** + * @covers ::filterByFields + */ + public function testFilterByFields() { + $account = $this->prophesize('\Drupal\Core\Session\AccountInterface')->reveal(); + $entity = $this->setupEntity($account); + + $constraint_list = $this->setupConstraintListWithoutCompositeConstraint($entity); + $violations = iterator_to_array($constraint_list); + + $this->assertSame($constraint_list->filterByFields(['name']), $constraint_list); + $this->assertCount(4, $constraint_list); + $this->assertArrayEquals(array_values(iterator_to_array($constraint_list)), [$violations[2], $violations[3], $violations[4], $violations[5]]); + } + + /** + * @covers ::filterByFields + */ + public function testFilterByFieldsWithCompositeConstraints() { + $account = $this->prophesize('\Drupal\Core\Session\AccountInterface')->reveal(); + $entity = $this->setupEntity($account); + + $constraint_list = $this->setupConstraintListWithCompositeConstraint($entity); + $violations = iterator_to_array($constraint_list); + + $this->assertSame($constraint_list->filterByFields(['name']), $constraint_list); + $this->assertCount(4, $constraint_list); + $this->assertArrayEquals(array_values(iterator_to_array($constraint_list)), [$violations[2], $violations[3], $violations[4], $violations[5]]); + } + + /** + * @covers ::filterByFieldAccess + */ + public function testFilterByFieldAccess() { + $account = $this->prophesize('\Drupal\Core\Session\AccountInterface')->reveal(); + $entity = $this->setupEntity($account); + + $constraint_list = $this->setupConstraintListWithoutCompositeConstraint($entity); + $violations = iterator_to_array($constraint_list); + + $this->assertSame($constraint_list->filterByFieldAccess($account), $constraint_list); + $this->assertCount(4, $constraint_list); + $this->assertArrayEquals(array_values(iterator_to_array($constraint_list)), [$violations[2], $violations[3], $violations[4], $violations[5]]); + } + + /** + * @covers ::filterByFieldAccess + */ + public function testFilterByFieldAccessWithCompositeConstraint() { + $account = $this->prophesize('\Drupal\Core\Session\AccountInterface')->reveal(); + $entity = $this->setupEntity($account); + + $constraint_list = $this->setupConstraintListWithCompositeConstraint($entity); + $violations = iterator_to_array($constraint_list); + + $this->assertSame($constraint_list->filterByFieldAccess($account), $constraint_list); + $this->assertCount(4, $constraint_list); + $this->assertArrayEquals(array_values(iterator_to_array($constraint_list)), [$violations[2], $violations[3], $violations[4], $violations[5]]); + } + + /** + * Builds the entity. + * + * @param \Drupal\Core\Session\AccountInterface $account + * An account. + * + * @return \Drupal\Core\Field\FieldItemListInterface + * A fieldable entity. + */ + protected function setupEntity(AccountInterface $account) { + $prophecy = $this->prophesize('\Drupal\Core\Field\FieldItemListInterface'); + $prophecy->access('edit', $account) + ->willReturn(FALSE); + $name_field_item_list = $prophecy->reveal(); + + $prophecy = $this->prophesize('\Drupal\Core\Field\FieldItemListInterface'); + $prophecy->access('edit', $account) + ->willReturn(TRUE); + $type_field_item_list = $prophecy->reveal(); + + $prophecy = $this->prophesize('\Drupal\Core\Entity\FieldableEntityInterface'); + $prophecy->hasField('name') + ->willReturn(TRUE); + $prophecy->hasField('type') + ->willReturn(TRUE); + $prophecy->get('name') + ->willReturn($name_field_item_list); + $prophecy->get('type') + ->willReturn($type_field_item_list); + + return $prophecy->reveal(); + } + + /** + * Builds an entity constraint violation list without composite constraints. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * A fieldable entity. + * + * @return \Drupal\Core\Entity\EntityConstraintViolationList + * The entity constraint violation list. + */ + protected function setupConstraintListWithoutCompositeConstraint(FieldableEntityInterface $entity) { + $violations = []; + + // Add two violations to two specific fields. + $violations[] = new ConstraintViolation('test name violation', '', [], '', 'name', 'invalid'); + $violations[] = new ConstraintViolation('test name violation2', '', [], '', 'name', 'invalid'); + + $violations[] = new ConstraintViolation('test type violation', '', [], '', 'type', 'invalid'); + $violations[] = new ConstraintViolation('test type violation2', '', [], '', 'type', 'invalid'); + + // Add two entity level specific violations. + $violations[] = new ConstraintViolation('test entity violation', '', [], '', '', 'invalid'); + $violations[] = new ConstraintViolation('test entity violation2', '', [], '', '', 'invalid'); + + return new EntityConstraintViolationList($entity, $violations); + } + + /** + * Builds an entity constraint violation list with composite constraints. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * A fieldable entity. + * + * @return \Drupal\Core\Entity\EntityConstraintViolationList + * The entity constraint violation list. + */ + protected function setupConstraintListWithCompositeConstraint(FieldableEntityInterface $entity) { + $violations = []; + + // Add two violations to two specific fields. + $violations[] = new ConstraintViolation('test name violation', '', [], '', 'name', 'invalid'); + $violations[] = new ConstraintViolation('test name violation2', '', [], '', 'name', 'invalid'); + + $violations[] = new ConstraintViolation('test type violation', '', [], '', 'type', 'invalid'); + $violations[] = new ConstraintViolation('test type violation2', '', [], '', 'type', 'invalid'); + + // Add two entity level specific violations with a compound constraint. + $composite_constraint = new EntityTestCompositeConstraint(); + $violations[] = new ConstraintViolation('test composite violation', '', [], '', '', 'invalid', NULL, NULL, $composite_constraint); + $violations[] = new ConstraintViolation('test composite violation2', '', [], '', '', 'invalid', NULL, NULL, $composite_constraint); + return new EntityConstraintViolationList($entity, $violations); + } + +}