Skip to content

Commit 2d7609d

Browse files
Validate field level constraints on form save (#400)
* Validate field level constraints on form save * #400 PR: code sniffer validations. Co-authored-by: Arlina Espinoza <[email protected]>
1 parent 73f1f3b commit 2d7609d

File tree

5 files changed

+131
-10
lines changed

5 files changed

+131
-10
lines changed

src/Entity/Form/FieldableEdgeEntityForm.php

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
2323
use Drupal\Core\Entity\Entity\EntityFormDisplay;
24+
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
2425
use Drupal\Core\Entity\EntityForm;
2526
use Drupal\Core\Entity\EntityInterface;
2627
use Drupal\Core\Form\FormStateInterface;
@@ -64,18 +65,100 @@ public function form(array $form, FormStateInterface $form_state) {
6465
/**
6566
* {@inheritdoc}
6667
*
67-
* @see \Drupal\Core\Entity\ContentEntityForm::buildEntity()
68+
* TODO Add missing return type-hint in 2.x.
6869
*/
69-
public function buildEntity(array $form, FormStateInterface $form_state) {
70-
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
71-
$entity = parent::buildEntity($form, $form_state);
70+
public function validateForm(array &$form, FormStateInterface $form_state) {
71+
parent::validateForm($form, $form_state);
72+
/** @var \Drupal\apigee_edge\Entity\FieldableEdgeEntityInterface $entity */
73+
$entity = $this->buildEntity($form, $form_state);
74+
75+
$violations = $entity->validate();
76+
77+
// Remove violations of inaccessible fields.
78+
$violations->filterByFieldAccess($this->currentUser());
79+
80+
// In case a field-level submit button is clicked, for example the 'Add
81+
// another item' button for multi-value fields or the 'Upload' button for a
82+
// File or an Image field, make sure that we only keep violations for that
83+
// specific field.
84+
$edited_fields = [];
85+
if ($limit_validation_errors = $form_state->getLimitValidationErrors()) {
86+
foreach ($limit_validation_errors as $section) {
87+
$field_name = reset($section);
88+
if ($entity->hasField($field_name)) {
89+
$edited_fields[] = $field_name;
90+
}
91+
}
92+
$edited_fields = array_unique($edited_fields);
93+
}
94+
else {
95+
$edited_fields = $this->getEditedFieldNames($form_state);
96+
}
97+
98+
// Remove violations for fields that are not edited.
99+
$violations->filterByFields(array_diff(array_keys($entity->getFieldDefinitions()), $edited_fields));
100+
101+
$this->flagViolations($violations, $form, $form_state);
72102

73-
// Mark the entity as requiring validation.
74-
$entity->setValidationRequired(!$form_state->getTemporaryValue('entity_validated'));
103+
// The entity was validated.
104+
$entity->setValidationRequired(FALSE);
105+
$form_state->setTemporaryValue('entity_validated', TRUE);
75106

76107
return $entity;
77108
}
78109

110+
/**
111+
* Gets the names of all fields edited in the form.
112+
*
113+
* If the entity form customly adds some fields to the form (i.e. without
114+
* using the form display), it needs to add its fields here and override
115+
* flagViolations() for displaying the violations.
116+
*
117+
* @param \Drupal\Core\Form\FormStateInterface $form_state
118+
* The current state of the form.
119+
*
120+
* @return string[]
121+
* An array of field names.
122+
*
123+
* @todo Add missing return type-hint in 2.x.
124+
*/
125+
protected function getEditedFieldNames(FormStateInterface $form_state) {
126+
return array_keys($this->getFormDisplay($form_state)->getComponents());
127+
}
128+
129+
/**
130+
* Flags violations for the current form.
131+
*
132+
* If the entity form customly adds some fields to the form (i.e. without
133+
* using the form display), it needs to add its fields to array returned by
134+
* getEditedFieldNames() and overwrite this method in order to show any
135+
* violations for those fields; e.g.:
136+
* @code
137+
* foreach ($violations->getByField('name') as $violation) {
138+
* $form_state->setErrorByName('name', $violation->getMessage());
139+
* }
140+
* parent::flagViolations($violations, $form, $form_state);
141+
* @endcode
142+
*
143+
* @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
144+
* The violations to flag.
145+
* @param array $form
146+
* A nested array of form elements comprising the form.
147+
* @param \Drupal\Core\Form\FormStateInterface $form_state
148+
* The current state of the form.
149+
*
150+
* @todo Add missing return type-hint in 2.x.
151+
*/
152+
protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
153+
// Flag entity level violations.
154+
foreach ($violations->getEntityViolations() as $violation) {
155+
/** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
156+
$form_state->setErrorByName(str_replace('.', '][', $violation->getPropertyPath()), $violation->getMessage());
157+
}
158+
// Let the form display flag violations of its fields.
159+
$this->getFormDisplay($form_state)->flagWidgetsErrorsFromViolations($violations, $form, $form_state);
160+
}
161+
79162
/**
80163
* {@inheritdoc}
81164
*/

tests/modules/apigee_edge_test/apigee_edge_test.module

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use Drupal\apigee_edge_test\Entity\Storage\DeveloperAppStorage;
3333
*/
3434
function apigee_edge_test_entity_type_alter(array &$entity_types) {
3535
/* @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
36-
foreach (_apigee_edge_entity_class_mapping() as $entity_type => $entity_class) {
36+
foreach (_apigee_edge_test_entity_class_mapping() as $entity_type => $entity_class) {
3737
if (isset($entity_types[$entity_type])) {
3838
$entity_types[$entity_type]->setClass($entity_class);
3939
}
@@ -48,7 +48,7 @@ function apigee_edge_test_entity_type_alter(array &$entity_types) {
4848
* @return array
4949
* Override map.
5050
*/
51-
function _apigee_edge_entity_class_mapping(): array {
51+
function _apigee_edge_test_entity_class_mapping(): array {
5252
return [
5353
'developer' => OverriddenDeveloper::class,
5454
'developer_app' => OverriddenDeveloperApp::class,

tests/modules/apigee_edge_test/src/Entity/OverriddenDeveloperApp.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,29 @@
2020
namespace Drupal\apigee_edge_test\Entity;
2121

2222
use Drupal\apigee_edge\Entity\DeveloperApp;
23+
use Drupal\Core\Entity\EntityTypeInterface;
2324

2425
/**
2526
* Class OverriddenDeveloperApp.
2627
*/
27-
final class OverriddenDeveloperApp extends DeveloperApp {}
28+
final class OverriddenDeveloperApp extends DeveloperApp {
29+
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public static function baseFieldDefinitions(EntityTypeInterface $entity_type): array {
34+
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $definitions */
35+
$definitions = parent::baseFieldDefinitions($entity_type);
36+
37+
// Set a length limit on app name that we can use in tests.
38+
$definitions['displayName']->setPropertyConstraints('value', [
39+
'Length' => [
40+
'min' => 1,
41+
'max' => 30,
42+
],
43+
]);
44+
45+
return $definitions;
46+
}
47+
48+
}

tests/src/Functional/DeveloperAppUITest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,23 @@ public function testCreateDuplicateApps() {
177177
$this->assertSession()->pageTextContains(static::DUPLICATE_MACHINE_NAME);
178178
}
179179

180+
/**
181+
* Tests that field validation constraints are executed on form save.
182+
*
183+
* @covers \Drupal\apigee_edge\Entity\Form\FieldableEdgeEntityForm::validateForm
184+
*/
185+
public function testFieldValidationConstraints() {
186+
/* @see \Drupal\apigee_edge_test\Entity\OverriddenDeveloperApp::baseFieldDefinitions() */
187+
$name = strtolower($this->randomMachineName(31));
188+
189+
$this->postCreateAppForm([
190+
'name' => $name,
191+
'displayName[0][value]' => $name,
192+
"api_products[{$this->products[0]->getName()}]" => $this->products[0]->getName(),
193+
]);
194+
$this->assertSession()->pageTextContains('This value is too long. It should have 30 characters or less.');
195+
}
196+
180197
/**
181198
* Tests creating two apps with the same name but different developers.
182199
*/

tests/src/Functional/OverriddenEntityClassTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class OverriddenEntityClassTest extends ApigeeEdgeFunctionalTestBase {
3232
public function testClassOverride() {
3333
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $manager */
3434
$manager = $this->container->get('entity_type.manager');
35-
foreach (_apigee_edge_entity_class_mapping() as $entity_type => $entity_class) {
35+
foreach (_apigee_edge_test_entity_class_mapping() as $entity_type => $entity_class) {
3636
$entity = $manager->getStorage($entity_type)->create();
3737
$this->assertInstanceOf($entity_class, $entity);
3838
}

0 commit comments

Comments
 (0)