Problem

The User module has a UserUniqueValidator whose job is to validate user fields like name and email for uniqueness. To do this, its validate() method must access contextual data other than the raw $value that is passed in. It currently does this with the following nasty code:
$field = $this->context->getMetadata()->getTypedData()->getParent();

Proposed resolution

Accessing the right data value is currently hard, as the constraints are put on the wrong layer. Instead of putting constraints on field item properties, we put them on the whole field. That way, the field is passed as value to the constraint validator and no further, complicated calls are required to get the right data.

Remaining tasks

-

User interface changes

-

API changes

-

Suggested commit message Issue #2110345 by Désiré, mr.york, fgm, fago, stefan.r, effulgentsia: Improve DX of validation constraints for fields.

Original report by effulgentsia

The User module has a UserUniqueValidator whose job is to validate user fields like name and email for uniqueness. To do this, its validate() method must access contextual data other than the raw $value that is passed in. It currently does this with the following nasty code:
$field = $this->context->getMetadata()->getTypedData()->getParent();

That's exposing way too much implementational detail of how Symfony's validation contexts, metadata for those contexts, and Drupal's TypedData systems are all stitched together. Instead, let's provide a base class with a protected getFieldItem() method to hide that detail from specific field validators.

#2002158: Convert form validation of comments to entity validation contains another use case for this, and surely, more will surface in core and contrib as the remaining entity validation tasks are worked through.

CommentFileSizeAuthor
#86 drupal8-validation_constraints_for_fields-2110345-86.patch8.82 KBeffulgentsia
#81 interdiff.txt2.41 KBeffulgentsia
#81 drupal8-validation_constraints_for_fields-2110345-81.patch10.18 KBeffulgentsia
#79 drupal8-validation_constraints_for_fields-2110345-79.patch10.17 KBeffulgentsia
#73 interdiff.txt1.67 KBpfrenssen
#73 drupal8-validation_constraints_for_fields-2110345-73.patch10.17 KBpfrenssen
#71 drupal8-validation_constraints_for_fields-2110345-71.patch10.6 KBpfrenssen
#68 drupal8-validation_constraints_for_fields-2110345-68.patch11.05 KBberdir
#62 drupal8-validation_constraints_for_fields-2110345-62.patch12.11 KBrajendar reddy
#58 2110345-validation_constraints_for_fields-58.patch12.13 KBmr.york
#52 interdiff-46-51.txt5.36 KBmr.york
#52 2110345-validation_constraints_for_fields-52.patch12.14 KBmr.york
#46 interdiff-43-46.txt1.17 KBmr.york
#46 2110345-validation_constraints_for_fields-46.patch12.31 KBmr.york
#43 2110345-validation_constraints_for_fields-43.patch12.36 KBmr.york
#41 interdiff-39-41.txt3.65 KBmr.york
#41 2110345-validation_constraints_for_fields-41.patch12.39 KBmr.york
#39 interdiff-30-39.txt5.4 KBmr.york
#39 2110345-validation_constraints_for_fields-39.patch12.36 KBmr.york
#30 interdiff-26-29.txt6.38 KBDésiré
#30 2110345-validation_constraints_for_fields-29.patch9.63 KBDésiré
#26 interdiff-24-26.txt3.7 KBDésiré
#26 2110345-validation_constraints_for_fields-26.patch8.08 KBDésiré
#24 0001-Issue-22110345-by-fago-stefan.r-fgm-Improve-field-va.patch10.34 KBfgm
#21 0001-Issue-22110345-by-fago-stefan.r-fgm-Improve-field-va.patch10.35 KBfgm
#19 0001-Issue-1696172-add-complex-merge-tests-for-deep-array.patch4.92 KBfgm
#13 d8_field_validation.patch8.2 KBfago
#10 d8_field_validation.patch9.06 KBfago
#8 drupal8.field_validation.2110345-8.patch9.06 KBstefan.r
#8 interdiff.txt987 bytesstefan.r
#4 d8_field_validation.patch9.04 KBfago
validation-field-item-aware.patch4.44 KBeffulgentsia

Comments

amateescu’s picture

Yep, this makes a lot of sense :)

  1. +++ b/core/lib/Drupal/Core/Validation/FieldItemAwareConstraintValidator.php
    @@ -0,0 +1,40 @@
    + * Validates the EntityChanged constraint.
    + */
    +abstract class FieldItemAwareConstraintValidator extends ConstraintValidator {
    

    The one line description must be updated.

  2. +++ b/core/lib/Drupal/Core/Validation/FieldItemAwareConstraintValidator.php
    @@ -0,0 +1,40 @@
    +      throw new \UnexpectedValueException('FieldItemAwareConstraintValidator::getFieldItem() called for data that is neither a field item nor a field item property.');
    

    I'm not sure about this one, should exceptions include the fully qualified class name?

moshe weitzman’s picture

Hiding details is good and this patch should continue on. But I don't consider this a complete fix to the problem. This code is a turd, and a turd hiding class like this one isn't a full resolution.

webchick’s picture

+1 subscribe.

fago’s picture

Issue summary: View changes
Issue tags: +Entity Field API
StatusFileSize
new9.04 KB

It currently does this with the following nasty code:
$field = $this->context->getMetadata()->getTypedData()->getParent();

True, but I think the DX problem is more caused by the validation constraints being not implement for field items, but field item properties. When the constraint actually deals with the field, it should be implemented on field level. If it deals just with a single value without any additional context, i.e. to check a regex, it fits well on the individual field property value.

Attached patch shows how the user constraint would be implemented on field item level - imho it's not that bad. You just get the field item object passed as value, what should be just fine. The only gotcha I see is that the field item might be NULL if it's empty, but that's the case for all constraint plugins and doing a

if (!isset($value)) {
  return;
}

in the beginning is what 90% of the symfony constraint validators already do also. (It's mostly NULL, Blank constraints that deal with the NULL value then - others should ignore them.)

In regard to the EntityChangedConstraint, I'm not sure we even need that. I'd see EntityChanged to be its own entity field type (not configurable), which
a) can care about setting/update the value as appropriately
b) can implement getConstraint() to return a constraint on the field, comparing it with last changed that of the entity. Thus, we could configure a regular compare-date constraint here and do not even need our own plugin.
Howsoever, that would totally deserve its own issue.

klausi’s picture

Status: Needs review » Reviewed & tested by the community

That patch makes a lot of sense, I totally agree that we should operate on the field item level for such context-sensitive constraints.

chx’s picture

I stiill see a + $field = $field_item->getParent(); which is undoubtly better but you still have to deal with typed data internals. Or getParent isn't internals?

berdir’s picture

getParent() is a TypedDataInterface yes, but there will be some more context specific method after #2002138: Use an adapter for supporting typed data on ContentEntities I guess.

That said, $field (which is actually a FieldItemList class) is only used to get the name of the field, wouldn't it be easier to understand if we get that from the field definition? $field_item->getFieldDefinition()->getFieldName() ?

stefan.r’s picture

StatusFileSize
new987 bytes
new9.06 KB
berdir’s picture

Thanks, I think that is easier to understand now.

fago’s picture

Title: Provide a FieldItemAwareConstraintValidator base class to hide typed data baggage from user-space code » Improve DX of validation constraints for fields
StatusFileSize
new9.06 KB
+++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserUniqueValidator.php
@@ -22,13 +22,13 @@
+    $field_name = $field_item->getFieldDefinition()->getFieldName(); ¶

Additional whitespace at the end of the line - fixed that.

fago’s picture

star-szr’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll

Tagging for reroll.

fago’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
StatusFileSize
new8.2 KB

re-rolled

Status: Needs review » Needs work

The last submitted patch, 13: d8_field_validation.patch, failed testing.

The last submitted patch, 13: d8_field_validation.patch, failed testing.

fago’s picture

Status: Needs work » Needs review

13: d8_field_validation.patch queued for re-testing.

fago’s picture

This needs some re-roll / test fixes

Status: Needs review » Needs work

The last submitted patch, 13: d8_field_validation.patch, failed testing.

fgm’s picture

Status: Needs work » Needs review
StatusFileSize
new4.92 KB

Rerolled with @covers / @coversDefaultClass annotations.

fgm’s picture

Oops, wrong window, sorry, ignore patch above.

fgm’s picture

Rerolled. For some reason, PHP fatals on my machine leaving no information behind. Let's see how it fares on the bot.

Status: Needs review » Needs work
andypost’s picture

fgm’s picture

Status: Needs work » Needs review
StatusFileSize
new10.34 KB

Rerolled without the typo.

Status: Needs review » Needs work
Désiré’s picture

Status: Needs work » Needs review
StatusFileSize
new8.08 KB
new3.7 KB

I have rerolled the patch, and fixed it. Now should not break the tests during the install.

Désiré’s picture

The main problem with #24 was, that the user name validation wasn't worked with a field instance as it works for email, but this wasn't the only problem which broke the installation: I'm also found an other bug here: #2226811: FieldItemBase type hints DataDefinitionInterface but requires ComplexDataDefinitionInterface

Status: Needs review » Needs work

The last submitted patch, 26: 2110345-validation_constraints_for_fields-26.patch, failed testing.

Désiré’s picture

Status: Needs work » Needs review
StatusFileSize
new9.63 KB
new6.38 KB
fago’s picture

This looks great, thanks! Imo, this is RTBC - but we'll need someone else to sign it off as I worked on it.

Désiré’s picture

If this is really close to commit, please include mr.york (https://drupal.org/user/52785) in the commit message, he helped me a lot in debugging.

aspilicious’s picture

+++ b/core/modules/user/lib/Drupal/user/Entity/User.php
@@ -473,8 +473,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+      // No Length constraint here because the UserName constraint also covers

This needs more spaces

mr.york’s picture

Add spaces.

andypost’s picture

@Désiré I've added commit message to summary

Just a few nitpicks

  1. +++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserMailUnique.php
    @@ -14,7 +14,8 @@
    + *   type = { "email" }
    
    +++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameConstraint.php
    @@ -14,7 +14,8 @@
    + *   type = { "string" }
    

    is this change described somewhere?
    I mean that I found no docs about "context" in annotation

  2. +++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameConstraintValidator.php
    @@ -18,11 +18,12 @@ class UserNameConstraintValidator extends ConstraintValidator {
    +  public function validate($field_item, Constraint $constraint) {
    
    +++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserUniqueValidator.php
    @@ -18,20 +18,22 @@ class UserUniqueValidator extends ConstraintValidator {
    +  public function validate($field_item, Constraint $constraint) {
    

    ConstraintValidatorInterface declares $value but $fieldItem makes more sense, but maybe it is a FieldItemList?

pwolanin’s picture

This looks ok, but I don't see that it actually accomplishes the change suggested in the summary.

Also, seems the last patch didn't get tested? maybe need to re-post.

Désiré’s picture

I mean that I found no docs about "context" in annotation

I think not, how can I do it?

ConstraintValidatorInterface declares $value but $fieldItem makes more sense, but maybe it is a FieldItemList?

Yes, I'll change this.

andypost’s picture

@Désiré the only pages I found is https://drupal.org/node/2015613
Also please check the change notices

mr.york’s picture

Renamed the $field_item values to $field_item_list in some validate function.

fago’s picture

+++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameConstraintValidator.php
@@ -18,11 +18,12 @@ class UserNameConstraintValidator extends ConstraintValidator {
+    $name = $field_item_list->value;

Oh, indeed the constraints are assigned to the item list level now. They could be assigned to the items by using
$field_definition->getItemDefinition()->setConstraints(). That does not seem so nice though, so staying at list level is fine to me.

Given that , we'd better use $list->first()->value then I guess.

mr.york’s picture

Given that , we'd better use $list->first()->value then I guess.

I replaced the relevant code.

Status: Needs review » Needs work

The last submitted patch, 41: 2110345-validation_constraints_for_fields-41.patch, failed testing.

mr.york’s picture

Rerolle #41.

mr.york’s picture

Status: Needs work » Needs review
fago’s picture

+++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameConstraint.php
@@ -14,7 +14,8 @@
+ *   type = { "string" }

+++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameUnique.php
@@ -14,7 +14,8 @@
+ *   type = { "string" }

This is wrong now, as you cannot put it on a string property, but only on a string field (the complete list). Unfortunately, we do not have data types per field type so there is no correct type to refer here except for the generic "list" :/

Maybe, we should do
type = {"list"} and
item_type = "field_item:string". I guess this needs more work/fixing, so let's better just keep it removed/missing for now.

mr.york’s picture

Maybe, we should do
type = {"list"} and
item_type = "field_item:string". I guess this needs more work/fixing, so let's better just keep it removed/missing for now.

Removed the " * type = { "string" }" annotation.

mr.york’s picture

pwolanin’s picture

Either the patch is incomplete or the issue summary is outdated, since a getFieldItem() method is not added

fago’s picture

Title: Improve DX of validation constraints for fields » Simplify validation constraint implementations for fields
Issue summary: View changes

Updated the summary. Patch looks ready to me. Once, we've got the base class filter out NULL values for us we could even add type hinting to our $value parameter, what should help DX a lot. But we'll need #2226821: Combine Drupal Constraint and ConstraintValidation classes into one for that first.

yched’s picture

+++ b/core/modules/comment/lib/Drupal/comment/Plugin/Validation/Constraint/CommentNameConstraintValidator.php
@@ -18,12 +18,12 @@ class CommentNameConstraintValidator extends ConstraintValidator {
-  public function validate($field_item, Constraint $constraint) {
-    $author_name = $field_item->value;
+  public function validate($field_item_list, Constraint $constraint) {
+    $author_name = $field_item_list->first()->value;

Throughout core, FieldItemList variables tend to be named $items, would be best if we stick to that ?

fago’s picture

Status: Needs review » Needs work

I've been thinking about that also, but I wasn't sure. But as you think the same way I guess we should then.

-> Let's use $items or $field_items as well.

mr.york’s picture

Status: Needs work » Needs review
StatusFileSize
new12.14 KB
new5.36 KB

Renamed the $field_item_list values to $items in relevant validate function.

mr.york’s picture

Désiré’s picture

mr.york’s picture

Désiré’s picture

Status: Needs review » Needs work

The last submitted patch, 52: 2110345-validation_constraints_for_fields-52.patch, failed testing.

mr.york’s picture

Status: Needs work » Needs review
StatusFileSize
new12.13 KB

Reroll.

fago’s picture

Thanks, re-roll / patch looks good to me.

mr.york’s picture

fago’s picture

Status: Needs review » Needs work
+++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserMailUnique.php
@@ -14,7 +14,8 @@
+ *   type = { "email" }

oh, I just figured that should not be added is the type is wrong. Just keep leaving it out as for the others.

rajendar reddy’s picture

Status: Needs work » Needs review
StatusFileSize
new12.11 KB

Updating patch with reroll. Please review.

torrance123’s picture

Both the prior state of the UserNameUnique validator and this patched version don't let me check for a unique username in the following way (following the pattern used in User.module::user_validate_name()):

function user_validate_name($name) {
  $definition = FieldDefinition::create('string')
    ->setConstraints(array('UserNameUnique' => array()));
  $data = \Drupal::typedDataManager()->create($definition);
  $data->setValue($name);
  $violations = $data->validate();
  if (count($violations) > 0) {
    return $violations[0]->getMessage();
  }
}

In the case of the patched version I get the error Fatal error: Call to a member function id() on a non-object in /srv/www/drupal8/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserUniqueValidator.php on line 29.

I'm not sure if this is because I'm using this validator incorrectly, or if this patch doesn't allow using these validators out of the context of a User entity.

fago’s picture

Issue tags: +sprint
fago’s picture

berdir’s picture

Status: Needs review » Needs work

The last submitted patch, 62: drupal8-validation_constraints_for_fields-2110345-62.patch, failed testing.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new11.05 KB

Re-roll.

pfrenssen’s picture

Straight reroll.

pfrenssen’s picture

@@ -114,11 +112,11 @@ function testValidation() {
-    // Provoke a email collision with an exsiting user.
+    // Provoke a email collision with an existing user.

If this is touched then also fix "a email collision" to "an email collision".

For the rest this looks RTBC to me.

pfrenssen’s picture

Fixed that little typo and cleaned up a few stray whitespace removals that crept in the patch. I didn't touch the code itself so if this comes back green it's RTBC.

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

Agreed - RTBC

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +TCDrupal 2014
+++ b/core/modules/user/user.module
@@ -17,7 +17,7 @@
+use Drupal\Core\Field\FieldDefinition;

@@ -332,7 +332,7 @@ function user_load_by_name($name) {
+  $definition = FieldDefinition::create('string')

This needs to be changed to BaseFieldDefinition

effulgentsia’s picture

I asked for a retest to ensure we have test coverage (i.e., a failing test) for what is pointed out in #75.

The last submitted patch, 73: drupal8-validation_constraints_for_fields-2110345-73.patch, failed testing.

effulgentsia’s picture

Status: Needs work » Needs review
StatusFileSize
new10.17 KB

Just a reroll. Does not fix #75, so should fail if there's test coverage of that line of code.

Status: Needs review » Needs work

The last submitted patch, 79: drupal8-validation_constraints_for_fields-2110345-79.patch, failed testing.

effulgentsia’s picture

Status: Needs work » Needs review
StatusFileSize
new10.18 KB
new2.41 KB

Excellent. An installation error is exactly what one would hope for when user_validate_name() tries to invoke code that doesn't exist :)

effulgentsia’s picture

+++ b/core/modules/user/src/Entity/User.php
@@ -461,14 +461,14 @@
-      ->setConstraints(array(
+      ->setPropertyConstraints('value', array(
-      ->setPropertyConstraints('value', array(
+      ->setConstraints(array(
-      ->setConstraints(array('UserMailUnique' => array()));
+      ->setPropertyConstraints('value', array('UserMailUnique' => array()));
-      ->setPropertyConstraints('value', array('UserMailUnique' => array()));
+      ->setConstraints(array('UserMailUnique' => array()));

Not sure why the interdiff includes the above, but if you look closely, those all cancel each other out. The user.module hunks are the only real changes.

fago’s picture

Status: Needs review » Reviewed & tested by the community

Indeed, that inderdiff looks strange. Anyway, changes are good - thanks, back to RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll

Needs a reroll.

+++ b/core/modules/user/src/Entity/User.php
@@ -468,7 +468,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
-      ->setPropertyConstraints('value', array(

@@ -483,7 +483,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
-      ->setPropertyConstraints('value', array('UserMailUnique' => array()));

We now have no usages of BaseFieldDefinition::setPropertyConstraints(). Shouldn't we get rid of it? Let's explore this in a followup.

effulgentsia’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
Related issues: +#2335633: Deprecate BaseFieldDefinition::setPropertyConstraints()
StatusFileSize
new8.82 KB

Rerolled. And filed followup for #85.

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

back to RTBC

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed e32a11e and pushed to 8.0.x. Thanks!

  • alexpott committed e32a11e on 8.0.x
    Issue #2110345 by mr.york, effulgentsia, Désiré, fgm, fago, pfrenssen,...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

almaudoh’s picture

Filed follow-up to further simplify defining unique field constraints at #2483869: Make Unique Field constraints generic