Buy

Validation with the UniqueEntity Constraint

Registration is working, but it's missing validation.

Since the form is bound to the User class, that is where our annotation rules should live. First, you need the use statement for the annotations. We added validation earlier in Genus. So, you can either copy this use statement, grab it from the documentation, or do what I do: cheat by saying use, auto-completing an annotation I know exists - like NotBlank, deleting the last part, and adding the normal as Assert alias:

116 lines src/AppBundle/Entity/User.php
... lines 1 - 7
use Symfony\Component\Validator\Constraints as Assert;
... lines 9 - 116

We obviously want email to be NotBlank. We also want email to be a valid email address. For plainPassword, that should also not be blank:

116 lines src/AppBundle/Entity/User.php
... lines 1 - 13
class User implements UserInterface
{
... lines 16 - 22
/**
* @Assert\NotBlank()
* @Assert\Email()
... line 26
*/
private $email;
... lines 29 - 36
/**
... line 38
* @Assert\NotBlank()
... lines 40 - 41
*/
private $plainPassword;
... lines 44 - 114
}

Pretty simple.

Ok, go back, keep the form blank, and submit. Nice validation errors.

Forcing a Unique Email

But check this out: type weaverryan+1@gmail.com. That email is already taken, so I should not be able to do this. But since there aren't any validation rules checking this, the request goes through and the email looks totally valid.

How can we add a validation rule to prevent that? By using a special validation constraint made just for this occasion.

The UniqueEntity Constraint

This constraint's annotation doesn't go above a specific property: it lives above the entire class. Add @UniqueEntity. Notice, this lives in a different namespace than the other annotations, so PhpStorm added its own use statement.

Next configure it. You can always go to the reference section of the docs, or, if you hold command, you can click the annotation to open its class. The public properties are always the options that you can pass to the annotation.

The options we need are fields - which tell it which field needs to be unique in the database - and message so we can say something awesome when it happens.

So add fields={"email"}. This is called fields because you could make this validation be unique across several columns. Then add message="It looks like you already have an account!":

118 lines src/AppBundle/Entity/User.php
... lines 1 - 4
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
... lines 6 - 10
/**
... lines 12 - 13
* @UniqueEntity(fields={"email"}, message="It looks like your already have an account!")
*/
class User implements UserInterface
... lines 17 - 118

Cool! Go back and hit register again. This just makes me happy!

We're good, right? Well, almost. There's one last gotcha with validation and registration.

Leave a comment!

  • 2017-04-17 Diego Aguiar

    Hey Ponrad!

    That's totally posible! you can adjust your message to something like this "user.email.unique"
    then in your translation file (In this case its a validation message, so you will need to create a file named "validators.us.yml" where "us" word changes depending on your locale parameter)


    //validators.us.yml
    user:
    email:
    unique: "It looks like your already have an account!"

    ProTip: Whenever you create a new translation file you have to clear your cache! but it's not needed when you are only modifying it

    Have a nice day!

  • 2017-04-15 Ponrad C

    HI is there any way to have the possibility to translate the message in annotation? For example in case when the site has several languages or similar


    @UniqueEntity(fields={"email"}, message="It looks like your already have an account!")


  • 2016-12-20 weaverryan

    Awesome :) - Merry Xmas back!

  • 2016-12-20 Stéphane

    Hey Ryan,
    Thank a lot for your answer. Your solution works fine.
    Cheers and merry Christmas.

  • 2016-12-19 weaverryan

    Hey Stèphane!

    Hmmm. So, when you setup something like this, it means that 2 users cannot have the same username+email combination. For example, you can I can't *both* have email: bob@free.fr & user: bob, but you and I *could* both have bob@free.fr, as long as we have different usernames. I'm wondering if this is the problem? If you want to make sure the user has a unique username AND also independently has a unique email, then try this:


    @UniqueEntity(fields={"username"})
    @UniqueEntity(fields={"email"})

    Let me know if that was it! Cheers!

  • 2016-12-19 Stéphane

    Hi,
    I have a bug about the double validation constraints.

    @UniqueEntity(fields={"username", "email"}

    and I use the same username and email, I have error 500 :

    An exception occurred while executing 'INSERT INTO demo_user (username, email, password, roles) VALUES (?, ?, ?, ?)' with params ["boba", "bob@free.fr", "$2y$13$Kx7Bhbuc40vrut1U34zGtuLow5JZRbayL6jZyItxNVI\/euHyRPnma", "[]"]:
    SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'bob@free.fr' for key 'UNIQ_7ADAAB3CE7927C74'

    When I use only one field, the validation work fine.

    Have you any idea about this problem ? Thank.

  • 2016-12-01 Victor Bocharsky

    haha, no problem! Twig calls this getter behind-the-scene when you call "{{ app.user.username }}" since username property is private - a little magic by Twig :)

  • 2016-12-01 Christophe Lablancherie

    Oh my gosh, i'm so stupid -_-

    Thanks Victor !

  • 2016-12-01 Victor Bocharsky

    So if I understand you right - the problem is that the username field holds a username in database, but when you try to output username property in Twig template with "{{ app.user.username }}" - you get the email value instead of username, right? So then you should check the getter method for username property in the first place, I bet the User::getUsername() method just returns incorrect property, i.e. "return $this->email", but should return "$this->username". ;)
    Let me know if it helps you.

    Cheers!

  • 2016-12-01 Christophe Lablancherie

    Hum, it's make sense about 50%....I have adapted the tutorial, and my users could log with username. But when i used the username, i see the email in the debug toolbar. I don't understand why...And even i retrieve the complete user (dump($this->getUser()), i can't access to the username in twig when i do app.user.username... I don't understand why...

    And just to specify it, at first time, i used FOSUserBundle, but i prefere to change because i've some bugs and i don't understand how it's works, it's a little bit too magical...

  • 2016-12-01 Victor Bocharsky

    Hey Christophe,

    Actually, username could be anything: user's nickname, email, phone number, etc. - it's up to you! In this tutorial we assume that user could log in by email and password, that's why when we say username we mean email. So you have to stop holding user's email on username property - just start to set username to the, actually, username value, i.e. user's nickname. That's it! If you also want to store user email besides the username, you need to add both $username and $email properties to the User class. Does it make sense for you?

    Cheers!

  • 2016-12-01 Christophe Lablancherie

    Hi,

    i followed the tutorial to the end and i two questions... ^^'

    - First : When i put this code in twig {{ app.user.username }} it's get me the email, and not the username. But when i debug $this->getUser() i get the complete user, with Username attribute...I don't know why it's not working... :(

    - Second : Why i'm log in with the email and not the username. I've followed the tutorial and i've modified the property for the Authenticator, but nothing changing...It's always the email and not username...

    providers:
    sng_user_provider:
    entity: { class: ******\UserBundle\Entity\User, property: username }

    Could you help me ?
    Thanks in advance !

  • 2016-10-31 Johan

    Works like a charm! It took us a while but we finally found the solution. Thank you Victor Bocharsky and weaverryan :) You guys are awesome.

  • 2016-10-31 weaverryan

    Johan! It all makes sense now :). Yes, it happens in handleRequest() Move your setPostedBy() to the beginning of the action method, e.g.:

    public function newPostAction(Request $request)
    {
    $post = new Post();
    $post->setPostedBy($this->getUser());
    $form = $this->createForm(PostForm::class, $post);

    $form->handleRequest($request);
    // ...
    }

    The way you did it is perfectly logical... I'm just so accustomed to doing all my data setup immediately that it didn't occur to me! Both ways are identical, but obviously, this way gives your form the data it needs for validation.

    Cheers!

  • 2016-10-31 Johan

    If my code in my controller looks like this:


    $form = $this->createForm(PostForm::class);
    $form->handleRequest($request);

    if($form->isValid()) {

    /** @var Post $post */
    $post = $form->getData();

    /** @var User $user */
    $user = $this->getUser();
    $post->setPostedBy($user);

    // persist/flush post here
    }

    then where is the validation done? I assume in the handleRequest method, right? How can it validate the @UniqueEntity without the User entity stored in the form/request yet?

  • 2016-10-31 weaverryan

    Hey Johan!

    Not exactly - validation is done *by* the form component, but all it does is say "validate this *object*" - so whatever data that is on your object (regardless of where it came from) is validated. So, this is still a mystery to me. You should at least see the query happening from the validator. You might actually open up the UniqueEntityValidator and do some debugging there - I can't remember having issues like this :/.

  • 2016-10-31 Johan

    I "attach" the user in the controller like:


    $user = $this->getUser();
    $post->setPostedBy($user);

    I think it makes sense that the validation gets ignored. Validation is done in the form object, not in the entity manager, right?

  • 2016-10-31 Johan

    I just did some testing with just single field uniqueness validation and this was the result:

    1.

    @UniqueEntity(fields={"postedAt"}, message="test message")

    works perfectly fine and it fails validation if "postedAt" already exists in the database. This 'postedAt' field is part of the form.

    2.

    @UniqueEntity(fields={"postedBy"}, message="test message") 

    does not work! It passes validation even if the 'postedBy' already exists. This 'postedBy' is not in the form (it's derived from the currently authenticated user).

    Is this the problem? All fields that I refer to in the @UniqueEntity annotation *must* be in the (Symfony) form?

  • 2016-10-31 Johan

    Thanks Ryan, I tried that before and it executes four queries:

    1. Query the currently authenticated user.
    2. Starting a transaction.
    3. Inserting into database.
    4. Rollback transaction. (because of the violation of the index).

    It doesn't seem to do any query to check for the @UniqueEntity annotation.

  • 2016-10-31 weaverryan

    Yea, I agree - this is weird, but I *feel* like the date field must be causing some sort of issue. Hours/minutes/seconds should *not* be there... but who knows. Behind the scenes, it's going to be doing a WHERE lookup by using the postedBy and postedDate - it's a little weird to do a WHERE matching an exact date, so it could be the issue. You could also check your web debug toolbar (even on the page where you hit the error) and click the Doctrine icon to see the queries for the page. This would at least let you see what query the UniqueConstraint is making to try to look for existing, matching Posts.

    Cheers!

  • 2016-10-31 Johan

    Yea, I did add that line to my code Victor.

    I'm thinking it might not work because I'm using dates. Maybe it still stores the hour/minute/second part of the date and so it thinks the dates are different. I feel like that wouldn't make too much sense because I'm using the 'date' type, not the 'datetime' type.

    Oh well, I think I will go with the custom validation callback, it should do the job. Thanks! :)

  • 2016-10-31 Victor Bocharsky

    Hey Johan,

    Did you use proper namespace for UniqueEntity constraint?


    use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

    /**
    * @ORM\Entity
    * @UniqueEntity(fields={"postedBy", "postedAt"}, message="Sorry, it looks like you already posted something on this day!")
    */
    class Post

    Anyways, I think you can add a custom validation callback instead - it should help.

    Cheers!

  • 2016-10-30 Johan

    Thank you Ryan, but I tried that already and it didn't do anything. It just passes the validation. This is what my code looks like (after removing the details and translating to the blog post example):


    ...
    /**
    * @ORM\Entity
    * @UniqueEntity(fields={"postedBy", "postedAt"}, message="Sorry, it looks like you already posted something on this day!")
    */
    class Post
    {
    ...

    /**
    * @Assert\Date()
    * @ORM\Column(type="date")
    */
    private $postedAt;


    /**
    * @ORM\ManyToOne(targetEntity="AppBundle\Entity\User", inversedBy="posts")
    * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
    */
    private $postedBy;

    ...
    }
  • 2016-10-30 weaverryan

    Yo Johan!

    Well, nice digging on this :). You CAN do this, it's with the same UniqueEntity annotation. The "fields" option takes an array, so you could have:


    @UniqueEntity(fields={"email"}, message="It looks like your already have an account!")

    If you ever get in a situation where the built-in constraints are not enough, then you'll need to build a custom validation constraint (http://symfony.com/doc/curr.... It's just a little bit of work, but works great and is needed quite rarely (we have exactly 2 in the entire KnpU codebase).

    Let me know if this is what you were looking for!

    Cheers!

  • 2016-10-28 Johan

    I see the name of the index in the message of the exception, but not stored in its own field or something like that. I could do some regex magic but that doesn't sound like a great solution to me. What if a new MySQL version uses different error messages?

  • 2016-10-28 Johan

    I found a solution but I don't think it's very great. I noticed the exception that gets thrown is UniqueConstraintViolationException so with a try catch block I detected whether the combination of "postedBy" and "postedAt" was invalid. Then I set errors on the form and return the original view. Like this:


    try {
    $em->persist($post);
    $em->flush();
    } catch(UniqueConstraintViolationException $e) {
    $form->get('postedAt')->addError(new FormError('You already posted something on this day!'));

    return $this->render('post/create.html.twig', ['form' => $form->createView()]);
    }

    The problem I'm having with this is that when I would add more of these constraints, I would never know which constraint was violated.

  • 2016-10-28 Johan

    Is there a way to have a unique combination instead of just one single unique field? Example: I don't want users to post more than 1 blog post a day. I would want the combination "postedBy" and "postedAt" to be unique. That would be great :)

    I can do this with Doctrine using a uniqueContraints key in the @Table annotation but this only adds this integrity check at the database layer. If I try to create a second post on one day, I just get a huge "Integrity constraint violation" exception.

    Is there a way to validate this combination and give the user a helpful message instead of blowing up?

  • 2016-10-15 Andrey Dvortsevoy

    Thanks

  • 2016-10-15 weaverryan

    Hi Andrey!

    About the roles problem. In our tutorial, we have a `roles` field, which is an array. In your User class, you have a `role` field, which is a string. That's ok! It simply means that you've setup your User to have a maximum of 1 role (which is ok!). But, you will need to change your `getRoles()` method to work a little bit different :). The problem, specifically, is that your getRoles() method references `$this->roles`, but you don't have this property (so this is null). Try this instead:


    public function getRoles()
    {
    $roles = array();

    // if the user has a role, add it to the array!
    if ($this->role) {
    $roles[] = $this->role;
    }

    // give everyone ROLE_USER!
    if (!in_array('ROLE_USER', $roles)) {
    $roles[] = 'ROLE_USER';
    }

    return $roles;
    }

    Let me know if this makes sense!

    Cheers!

  • 2016-10-15 Andrey Dvortsevoy

    All works a current in base data in a floor roles there is no value
    At addition value ROLE_ADMIN there is mistake Warning: in_array () expects parameter 2 to be array, null givenи specifies
    Entity\User.php at line 78 -


    $roles = $this->roles;

    // give everyone ROLE_USER!
    if (!in_array('ROLE_USER', $roles)) {
    $roles[] = 'ROLE_USER';
    }
  • 2016-10-14 Victor Bocharsky

    Hey Andrey,

    Your class annotation looks valid, I hope you don't forget to add use Doctrine\ORM\Mapping as ORM; line before the class User declaration. But I don't see @UniqueEntity() annotation for this class, did you add it? I mean, what annotation are caused the error you mentioned before? And please, make sure you have an email field in the table of User class.

    Cheers!

  • 2016-10-13 Andrey Dvortsevoy

    class User implements UserInterface
    {
    /**
    * @ORM\Id
    * @ORM\Column(type="integer")
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    private $id;

    /**
    * @var string
    * @Assert\Email()
    * @ORM\Column(type="string", unique=true)
    * @Assert\NotBlank()
    */
    private $email;

    /**
    * @var string
    * @ORM\Column(type="string", length=50)
    * @Assert\NotBlank()
    */
    private $name;

    /**
    * @var string
    * @ORM\Column(type="string", length=50)
    */
    private $role;

    /**
    * @var
    * @Assert\Length(max="4096")
    * @Assert\NotBlank()
    */
    private $plainPassword;

    /**
    * @var string
    * @ORM\Column(type="string", length=64)
    */
    private $password;
  • 2016-10-13 Victor Bocharsky

    Hey Andrey,

    Please, ensure you type exactly php bin/console doctrine:schema:update --force: it should be `--force`, i.e. with double dash `--`! If it doesn't help - make sure you have a proper mapping: the most common error is missing the @ORM prefix, i.e. it should be something like @ORM\Column(type="string") above $email property and its class uses proper namespace: use Doctrine\ORM\Mapping as ORM;.

    Cheers!

  • 2016-10-13 Andrey Dvortsevoy

    At pressing on php bin/console doctrine:schema:update - force in a database why that automatically is not created a field email, and the field id is created a current.

  • 2016-10-13 Victor Bocharsky

    Hey Andrey,

    Do you map $email property to the database, i.e. do you have $email property and email field in the database for the entity which you're trying to validate?

    Cheers!

  • 2016-10-12 Andrey Dvortsevoy

    To see the error as click on the check
    The field "email" is not mapped by Doctrine, so it cannot be validated for uniqueness.
    How to solve?

  • 2016-10-11 Vlad

    Thank you, Victor, I'll have a look at that.

  • 2016-10-11 Victor Bocharsky

    Ah, no, unfortunately I don't. But here's a link I found: http://symfony.com/doc/curr... . Also take a look at this test: https://github.com/symfony/... .

    BTW, there're a few bundles which probably could help you with it:
    https://github.com/lexik/Le...
    https://github.com/maschman...

    Cheers!

  • 2016-10-11 Vlad

    Thanks, Victor! Do you happen to have an example of that at hand?

  • 2016-10-11 Victor Bocharsky

    You can also store translations in a database (or any other storage). In this case you should provide a custom loader implementing the LoaderInterface interface. And your loader should be defined as a service with a translation.loader tag. But except the custom loader, everything is the same.

    Cheers!

  • 2016-10-11 Vlad

    Hello, Victor!
    What if I want the message to be fetched from the database?
    Thanks and 73,

  • 2016-10-11 Victor Bocharsky

    Hey, the best way is to use translation component like here: https://knpuniversity.com/s... , but better to use translation message key instead, something like:


    /**
    * @UniqueEntity(fields={"email"}, message="user.email_already_taken")
    */
    class User implements UserInterface

    And translate this key in validation messages:

    #app/Resources/translations/validators.en.yml
    user.email_already_taken: It looks like your already have an account!

    The Form component translate this message key for you if you'll have a translation message for key.

    Cheers!

  • 2016-10-10 Vlad

    Hi Ryan,
    How can I pass dynamic messages (e.g. from the database) to these annotations? Let's say I want internationalization here. Is it possible?
    Thanks and 73,