Problem/Motivation

After #2403817: Feed entity validation misses form validation logic, a unique field value validator was created to simplify and unify definition of base fields. However, to define a unique constraint one still needs to create a new Constraint class and use the same boilerplate code viz:

class UserMailUnique extends Constraint {

  public $message = 'The email address %value is already taken.';

  /**
   * {@inheritdoc}
   */
  public function validatedBy() {
    return '\Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldValueValidator';
  }
}
class FeedUrlConstraint extends Constraint {

  public $message = 'A feed with this URL %value already exists. Enter a unique URL.';

  /**
   * {@inheritdoc}
   */
  public function validatedBy() {
    return '\Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldValueValidator';
  }

}

The unique constraint will be used often in contrib for defining entities (I already have 3 use-cases) and it doesn't make sense to create a little new class each time.

Proposed resolution

Make a new generic UniqueField constraint that can be reused in all field definitions. This replaces the existing FeedTitleConstraint, FeedUrlConstraint, UserMailUnique and UserNameUnique constraints which are virtually clones. We use this new constraint for block content as well, which used to have odd, form-only validation.

We can now define constraints in addConstraint() as opposed to in separate classes. The unique field constraint is used to fix a bug in BlockContentForm where entity validation is still done in the form.

Remaining tasks

Review
Commit

User interface changes

None

API changes

A new UniqueField constraint.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task (although the form-only validation for block content is arguably a bug)
Issue priority Major because fixes custom form-only validation for block content
Prioritized changes This issue reduces fragility and improves usability of the unique field constraint.
Disruption Not disruptive for core. Would be disruptive for contributed and custom modules/themes if they used any of UserMailUnique, UserNameUnique, FeedTitleConstraint or FeedUrlConstraint in their field definitions - this is highly unlikely though.
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

almaudoh’s picture

Status: Active » Needs review
FileSize
6.03 KB

Here goes.

Berdir’s picture

Having a generic implementation for this would be great.

Core has one more use case that is currently not implemented as a constraint at all.

See BlockContentForm::validateForm(). That's weird, custom, form-only validation code.

stefan.r’s picture

I'll go ahead and add a BlockContentDescriptionConstraint, seems like a good way to test if the patch works while the testbot isn't testing :)

stefan.r’s picture

Title: Make Unique Field constraints generic » Make Unique Field constraints generic and use this everywhere
FileSize
3.82 KB
10.6 KB

Well adding a constraint on the block description worked quite nicely, so your patch makes sense :)

The generic test message doesn't have test coverage and the placeholders aren't easily override-able from the unique field constraints themselves but maybe that's for the better, as people only really need the field value when validating uniqueness.

stefan.r’s picture

FileSize
1.46 KB
12.23 KB

And a test

almaudoh’s picture

I'm thinking we can improve this further by removing all the clone classes and figure out a way to override the message in the BaseFieldDefinition::addConstraint() where necessary.

That way we could get rid of FeedTitleConstraint, FeedUrlConstraint, UserMailUnique, UserNameUnique and even BlockContentDescriptionConstraint and use the single UniqueFieldConstraint in the base field definitions.

So something like

BaseFieldDefinition::create('integer')->addConstraint('UniqueField', ['message' => 'A custom duplicate entry message']);
stefan.r’s picture

Sounds like a good idea! I asked about this on IRC and apparently @Berdir had already tried to push for that in other issues but it didn't end up happening because people just wanted to get the issue done.

almaudoh’s picture

Like so. Removed all the clones and used a single UniqueField constraint for all of them. Also added a test to verify that the user email validator works.

stefan.r’s picture

Nice work! Had a look at the interdiff and don't see any problem with this at first sight. If this passes tests, +1 :)

Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldConstraint.php
    @@ -0,0 +1,31 @@
    +  public $message = 'The %entity_type_id with the %field_name %value already exists.';
    

    Shouldn't we use the entity type label here, and not the id? There is a method to get the lowercase label I think. It's actually the same string for user in english, but not other languages, and for example it's block_content vs "custom block".

  2. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -187,7 +187,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    -      ->setDisplayConfigurable('form', TRUE);
    +      ->setDisplayConfigurable('form', TRUE)
    +      ->addConstraint('UniqueField', ['message' => 'A block with description %value already exists.']);
     
    

    Ah, you use custom messages then to replace the message completely. sneaky ;) Still, the default should be better I'd say :)

  3. +++ b/core/modules/user/src/Tests/UserCreateTest.php
    @@ -115,4 +115,22 @@ public function testUserAdd() {
    +  /**
    +   * Tests the unique user email constraint.
    +   */
    +  public function testDuplicateUserEmail() {
    +    $user1 = $this->drupalCreateUser();
    

    We already have tests for this in UserValidationTest I think.

almaudoh’s picture

#10:
1. Was thinking about that too. Done.
2. :) Didn't know which tests were based on exact validation messages. I prefer the default message also.
3. Didn't see that. Removed the new test.

almaudoh’s picture

#10.2: We can remove some (or all) of the custom messages in a later patch after testbot comes back.

The last submitted patch, 4: 2483869-4.patch, failed testing.

The last submitted patch, 5: 2483869-5.patch, failed testing.

The last submitted patch, 8: make_unique_field-2483869-7.patch, failed testing.

stefan.r’s picture

Tests are green, I think you can remove the custom messages now wherever it makes sense.

IMO we shouldn't remove all of them, but most can be removed.

almaudoh’s picture

Ok. Testbot is back. Removes custom messages from the username and user email constraints to see what breaks. I still like to retain some of the custom messages as examples for developers of how easily the messages can be overridden.

almaudoh’s picture

stefan.r’s picture

I think we can lose the empty array in the second argument of addConstraint(). We also shouldn't be deleting that newline before the end of the closing brace of the class definition.

As to what will break, you could just do a grep for the error message and replace the verbiage in the relevant tests.

This will also need a beta evaluation.

Status: Needs review » Needs work

The last submitted patch, 17: make_unique_field-2483869-16.patch, failed testing.

almaudoh’s picture

Status: Needs work » Needs review
FileSize
18.08 KB
4.88 KB

Fixed verbiage in the failing tests and implemented lower case fieldname in the violation error message as well.

We also shouldn't be deleting that newline before the end of the closing brace of the class definition.

This newline was inadvertently introduced by me when I added tests to UserCreateTest and is out of scope.

almaudoh’s picture

I think we can lose the empty array in the second argument of addConstraint()

Forgot that. Fixed now and some more validation error verbiage in the tests. Sorry for the noise.

The last submitted patch, 21: make_unique_field-2483869-21.patch, failed testing.

stefan.r’s picture

  1. +++ b/core/lib/Drupal/Core/Field/FieldConfigInterface.php
    @@ -178,7 +178,7 @@ public function addPropertyConstraints($name, array $constraints);
    -   *   $fields['field_username']->addConstraint('UserNameUnique', []);
    +   *   $fields['field_username']->addConstraint('UniqueField', []);
    

    This doesn't need a second argument either, or maybe use a custom message in the example?

  2. +++ b/core/modules/block_content/src/Tests/BlockContentCreationTest.php
    @@ -66,8 +66,8 @@ public function testBlockContentCreation() {
    -    $this->assertRaw(format_string('A block with description %name already exists.', array(
    -      '%name' => $edit['info[0][value]']
    +    $this->assertRaw(format_string('The custom block with the block description %value already exists.', array(
    

    I wonder if the custom verbiage wasn't clearer than the automatic one. But maybe that just means we need to change the labels on the entity? (ie. s/custom block/block/)

  3. +++ b/core/modules/user/src/Tests/UserRegistrationTest.php
    @@ -141,13 +141,13 @@ function testRegistrationEmailDuplicates() {
    -    $this->assertText(t('The email address @email is already taken.', array('@email' => $duplicate_user->getEmail())), 'Supplying an exact duplicate email address displays an error message');
    +    $this->assertText(t('The user with the email @email already exists.', array('@email' => $duplicate_user->getEmail())), 'Supplying an exact duplicate email address displays an error message');
    ...
    -    $this->assertText(t('The email address @email is already taken.', array('@email' => $duplicate_user->getEmail())), 'Supplying a duplicate email address with added whitespace displays an error message');
    +    $this->assertText(t('The user with the email @email already exists.', array('@email' => $duplicate_user->getEmail())), 'Supplying a duplicate email address with added whitespace displays an error message');
    

    In the examples I've quoted so far "A %entity_type" would've been more correct than "The %entity type". Maybe that would be better for a default message? Also does anyone know, have we standardized on "email" or "email address"? We shouldn't be mixing those up...

almaudoh’s picture

In the examples I've quoted so far "A %entity_type" would've been more correct than "The %entity type".

Was trying to avoid cases where an entity name like "apple" would result in "A apple with the name...." which sounds weird. Guess that's where the custom message becomes useful.

stefan.r’s picture

Hmm good point. "A/An" would look as odd as "The", and coding the grammar rules would be a bit of a convoluted way of avoiding custom messages.

Maybe we should stick with the custom messages then as they're more descriptive/end user-friendly, maybe @Berdir can chime in as he had suggested using the defaults.

Berdir’s picture

I'm OK with keeping the custom messages. It's still a huge simplification compared to what we have now. Generic messages like that are always hard and they are very hard to translate as well, usually.

almaudoh’s picture

I don't really know how strings that are not in t() or $this->t() are translated, so another question is: would the custom strings be translatable?

Berdir’s picture

There is explicit support to translate the default string, but the custom strings need to be passed through t() explicitly, like the labels/descriptions on the field definitions.

stefan.r’s picture

Well @almaudoh if you have that doubt I'm sure others would have it as well, so wrapping custom messages in t() would remove that doubt :)

Also not sure it's worth making custom messages translated by default given unique constraints are a rarity and how little effort it is to just wrap a message in t(), which people are used to anyway.

+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
@@ -37,7 +38,11 @@ public function validate($items, Constraint $constraint) {
+        '@entity_type' => $entity->getEntityType()->getLowercaseLabel(),
+        '@field_name' => Unicode::strtolower($items->getFieldDefinition()->getLabel()),

May also be worth checking if the labels are already translated (I guess so)?

almaudoh’s picture

So reverted all the original custom messages. Changed default message to "A %entity_type...", entities where that doesn't work can always use custom messages. Also added custom message to the FieldConfigInterface comment example.

stefan.r’s picture

Just looking at the interdiff and this looks great to me now.

Technically maybe the tests should say t() instead of format_string() but those tests are run in English anyway so I don't think that's worth changing.

almaudoh’s picture

Forgot to make Feed custom message translatable also.

almaudoh’s picture

Technically maybe the tests should say t() instead of format_string() but those tests are run in English anyway so I don't think that's worth changing.

t() is not encouraged in tests due to the dependencies.

The last submitted patch, 31: make_unique_field-2483869-31.patch, failed testing.

stefan.r’s picture

Issue summary: View changes

Updating issue summary

stefan.r’s picture

@Berdir do you think any of this is Major? Or that "reduces fragility" might apply here?

almaudoh’s picture

Title: Make Unique Field constraints generic and use this everywhere » Make Unique Field constraints generic and use it to fix BlockContent's form-only entity validation
Issue summary: View changes
Priority: Normal » Major
Issue tags: +Entity Field API

I think it should be major on account of the BlockContent entity validation fix. And "reduces fragility" also applies.

Berdir’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -187,7 +187,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +      ->setDisplayConfigurable('form', TRUE)
    +      ->addConstraint('UniqueField', ['message' => t('A block with description %value already exists.')]);
    

    Hm, actually, this doesn't work.

    This will immediately translate without replacing the placeholder. So you will have a german string that still has the placeholder. Then we translate it again, using the german string as translation source.

    This is one of the cases where we, instead of actually translating, just need to mark it as a translatable string, which is unfortunately something that we still don't have.

  2. +++ b/core/modules/block_content/src/Tests/BlockContentCreationTest.php
    @@ -66,8 +66,8 @@ public function testBlockContentCreation() {
    +    $this->assertRaw(format_string('A block with description %value already exists.', array(
    +      '%value' => $edit['info[0][value]']
    

    Can we switch this to SafeMarkup::format() when we touch it anyway? format_string() never got a @deprecated, but it really is..

stefan.r’s picture

Issue summary: View changes

@almaudoh yes that makes sense, updated issue summary.

As to the translation problem, indeed the translated strings would be retranslated in ExecutionContext::addViolation() so we could remove the t()'s but as @Berdir mentions then the potx extractor wouldn't find them, so probabably something like #1542144: Mark strings as localizable/translatable (new t()-alike string function that isn't t(), only for potx) / #2363099: Using translated field definition descriptions in entity schema results in different schema definitions, resulting in update.php changes would be the cleanest way to fix?

almaudoh’s picture

Looks like the translation issue won't be fixed soonest.

We can revert the "clones" and then remove them in a follow up after translation issue is fixed. Thoughts?

larowlan’s picture

almaudoh’s picture

Another way we could fix this without the dependency on the translation issue is to just accept the generic violation messages, which are okay IMHO, adjust the strings in the tests and be done with it.

almaudoh’s picture

Status: Needs work » Needs review
FileSize
12.92 KB
6.01 KB

Trying to move this issue forward:
1. Re-instated the sub-classes of UniqueFieldConstraint (FeedUrl, FeedName, UserNameUnique and UserMailUnique) to cater for existing specific violation messages. Note that these sub-classes exist only because we've not yet resolved the translation issue for custom dynamic messages. That sucks, IMHO.
2. I didn't create a new subclass for BlockContent, which is why the violation message looks weird.

We can fix 1) and 2) in follow-ups.

almaudoh’s picture

Missed one test assert...

The last submitted patch, 46: make_unique_field-2483869-46.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 47: make_unique_field-2483869-47.patch, failed testing.

The last submitted patch, 47: make_unique_field-2483869-47.patch, failed testing.

almaudoh’s picture

Status: Needs work » Needs review
FileSize
623 bytes
12.93 KB

Ok. The patch still applies. This fixes the test fail.

almaudoh’s picture

stefan.r’s picture

Attempt at a reroll

Status: Needs review » Needs work

The last submitted patch, 55: 2483869-55.patch, failed testing.

almaudoh’s picture

swentel’s picture

Status: Needs work » Needs review
FileSize
13.13 KB
stefan.r’s picture

FileSize
3.18 KB

looks great

stefan.r’s picture

Title: Make Unique Field constraints generic and use it to fix BlockContent's form-only entity validation » Make Unique Field constraints generic
jibran’s picture

Status: Needs review » Reviewed & tested by the community

LGTM

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Less boilerplate will help people make less mistakes. Committing under the committer discretion provision in the beta policy. Committed c73fdfc and pushed to 8.0.x. Thanks!

  • alexpott committed c73fdfc on 8.0.x
    Issue #2483869 by almaudoh, stefan.r, swentel, Berdir: Make Unique Field...

Status: Fixed » Closed (fixed)

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