Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

UniqueEntity & Validation Directly on Form Fields

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

The registration form works, but we have a few problems. First, geez, it looks terrible. We'll fix that a bit later. More importantly, it completely lacks validation... except, of course, for the HTML5 validation that we get for free. But, we can't rely on that.

No problem: let's add some validation constraints to email and plainPassword! We know how to do this: add annotations to the class that is bound to this form: the User class. Find the email field and, above, add @Assert\NotBlank(). Make sure to hit tab to auto-complete this so that PhpStorm adds the use statement that we need on top. Also add @Assert\Email().

... lines 1 - 14
class User implements UserInterface
{
... lines 17 - 23
/**
... lines 25 - 26
* @Assert\NotBlank()
* @Assert\Email()
*/
private $email;
... lines 31 - 248
}

Nice! Move back to your browser and inspect the form. Add the novalidate attribute so we can skip HTML5 validation. Then, enter "foo" and, submit! Nice! Both of these validation annotations have a message option - let's customize the NotBlank message: "Please enter an email".

... lines 1 - 23
/**
... lines 25 - 26
* @Assert\NotBlank(message="Please enter an email")
... line 28
*/
private $email;
... lines 31 - 250

Cool! email field validation, done!

Unique User Validation

But... hmm... there's one other validation rule that we need that's related to email: when someone registers, we need to make sure their email address isn't already registered. Try geordi@theenterprise.org again. I'll add the novalidate attribute so I can leave the password empty. Register! It explodes!

Integrity constraint violation: duplicate entry "geordi@theenterprise.org

Ok, fortunately, we do have the email column marked as unique in the database. But, we probably don't want a 500 error when this happens.

This is the first time that we need to add validation that's not just as simple as "look at this field and make sure it's not blank", "or a valid email string". This time we need to look into the database to see if the value is valid.

When you have more complex validation situations, you have two options. First, try the Callback constraint! This allows you do whatever you need. Well, mostly. Because the callback lives inside your entity, you don't have access to any services. So, you couldn't make a query, for example. If Callback doesn't work, the solution that always works is to create your very own custom validation constraint. That's something we'll do later.

Fortunately, we don't need to do that here, because validating for uniqueness is so common that Symfony has a built-in constraint to handle it. But, instead of adding this annotation above your property, it lives above your class. Add @UniqueEntity. Oh, and notice! This added a different use statement because this class happens to live in a different namespace than the others.

This annotation needs at least one option: the fields that, when combined, need to be unique. For us, it's just email. You'll probably want to control the message too. How about: I think you've already registered.

... lines 1 - 7
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
... lines 9 - 12
/**
... line 14
* @UniqueEntity(
* fields={"email"},
* message="I think you're already registered!"
* )
*/
class User implements UserInterface
... lines 21 - 255

Oh, and just a reminder: if you have the PHP annotations plugin installed, you can hold command or control and click the annotation to open its class and see all its options.

Let's try it! Move over and refresh! Got it! That's a much nicer error.

Adding Validation Directly to Form Fields

There is one last piece of validation that's missing: the plainPassword field. At the very least, it needs to be required. But, hmm. In the form, this field is set to 'mapped' => false. There is no plainPassword property inside User that we can add annotations to!

No problem. Yes, we usually add validation rules via annotations on a class. But, if you have a field that's not mapped, you can add its validation rules directly to the form field via a constraints array option. What do you put inside? Remember how each annotation is represented by a concrete class? That's the key! Instantiate those as objects here: new NotBlank(). To pass options, use an array and set message to Choose a password!.

Heck, while we're here, let's also add new Length() so we can require a minimum length. Hold command or control and click to open that class and see the options. Ah, yea: min, max, minMessage, maxMessage. Ok: set min to, how about 5 and minMessage to Come on, you can think of a password longer than that!

... lines 1 - 12
class UserRegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 18 - 20
->add('plainPassword', PasswordType::class, [
'mapped' => false,
'constraints' => [
new NotBlank([
'message' => 'Choose a password!'
]),
new Length([
'min' => 5,
'minMessage' => 'Come on, you can think of a password longer than that!'
])
]
]);
;
}
... lines 35 - 41
}

Done! These constraint options will work exactly the same as the annotations. To prove it, go back and refresh! Got it! Now, validating an unmapped field is no problem. We rock!

Next: the registration form is missing one other field: the boring, but, unfortunately, all-important "Agree to terms" checkbox. The solution... is interesting.

Leave a comment!

20
Login or Register to join the conversation

Why am I getting 500 errors instead of just showing the errors to the user ?

InvalidArgumentException HTTP 500 Internal Server Error<br />Expected argument of type "string", "NULL" given at property path "username".

I have the Assert on the User and using the same code as you
@Assert\NotBlank(message="Please enter a username")

<b>Edit : Turns out you need to allow your class to be in an invalid state to let Symfony manage errors for forms bound to entities. You have to set the return type of your methods to "?string" for example, even if the property is required and should never be null. This lets Symfony set the property to an invalid value (like null for an email adress) and then errors will show up on your form</b>

1 Reply

Hey Leif,

Yes, you're correct! It's required by design by validator. Though, you can bypass allowing entity to be in invalid state with form DTO: https://symfonycasts.com/sc... - it will be further in this course. But this way requires more work, so it's still controversial question.

Thank you for sharing your solution with others btw!

Cheers!

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 2 years ago

@Assert is not working for me.

Reply

Hey @Farry7!

Sorry to hear that! Are you getting an error? Or is it simply not working? Also make sure that you've installed the validator component - composer require validator - there was one other comment that made me think that this *could* be a possible problem.

Cheers!

Reply
Gabriel F. Avatar
Gabriel F. Avatar Gabriel F. | posted 3 years ago

Getting: Expected argument of type "string", "null" given at property path "username".
Instead of: form with error labels after submitted

Hello Guys! My validations are not working in the form. Another person that seems to have the same problem solve by adding a question mark to the type the getter is returning, something like: "getUsername(): ?string" to the entity getter.

Reply

Hey Gabriel F.

If you have this error on form submitting, then it's caused by setUsername(string $username) and to solve it you need to add question mark to it like: setUsername(?string $username).

IIRC latest version of maker bundle adds nullable mark to getter and setter by default.

Cheers!

Reply

How can I allow users to change info on their profile without asking them to retype the password?

This is also a problem for admins that manages user data when there is a password field there.

Reply

Hey lesleyfernandes!

GREAT question. Validation groups :). Basically, when you create the "profile" form, you can set that form's validation group to something like "update". Then you can use groups to make the NotBlank on password not checked for the edit profile form.

We have a nice example of this inside API Platform: https://symfonycasts.com/sc... - it's not using a form, but it shows how you can group some your constraints. The only difference in your case is that you will need to configure your edit profile form to validate in, for example, the "update" and "Default" groups - https://symfony.com/doc/cur...

Let me know if that helps!

Cheers!

Reply
Peter-K Avatar
Peter-K Avatar Peter-K | posted 3 years ago | edited

I am creating form in controller without binding it to entity (using data_class). Like follow


$form = $this->createFormBuilder([],[
            'csrf_protection' => false,
        ])
            ->add('action', ChoiceType::class, [
                'choices' => [
                    self::ACTION_MARK_AS_REVIEWED,
                    self::ACTION_MARK_AS_NOT_REVIEWED,
                ],
                'label' => false,
                'expanded' => true,
                'multiple' => false,
                'constraints' => [
                    new NotBlank(),
                ],
            ])
            ->add('comment',null)
            ->getForm();

I want to add constraint based on action something like below

if (action field == Not Reviewed){ addViolation/constrain to comment field NotBlank).```


I checked documentation and all examples are based on putting callback validation inside of Entity class but I am not using entity for this scenario so how can I achieve this and validate comment based on action field value?
Reply

Hey Peter K.!

Cool question :). I had to do some digging - and I think this can be done easily (it's just not a case I had thought of before)! Here's my idea:

1) Pass a constraints option... but to the entire form itself - not just a single field. You'll do this down in configureOptions().

2) Inside this, use the new Callback() and pass this a Closure. I think it will look like this:


public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'constraints' => [
            new Callback(function($data, ExecutionContextInterface $context, $payload) {
                // do your magic here!
            }
        ]
    ]);
}

I'm making several assumptions here... so I could be totally wrong. But, if you have a few minutes, let me know. It's actually a really compelling idea - a custom function for extra validation on your form type. I would love this to work :).

Cheers!

Reply
Peter-K Avatar
Peter-K Avatar Peter-K | weaverryan | posted 3 years ago | edited

Thanks weaverryan

I have it working when I create separate form entity with just these 2 fields.

However I couldnt figure it out how would I do that in controller.


public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'constraints' => [
                new Callback(function($data, ExecutionContextInterface $context, $payload){
                    if($data['action'] == Controller::ACTION_MARK_AS_NOT_REVIEWED){
                        if(trim($data['comment']) == ''){
                            $context->buildViolation('This value should not be blank.')
                                ->atPath('comment')
                                ->addViolation();
                        }
                    }
                })
            ],
        ]);
    }

where would you paste above code for this form in controller?


$form = $this->createFormBuilder([],[
            'csrf_protection' => false,
        ])
            ->add('action', ChoiceType::class, [
                'choices' => [
                    self::ACTION_MARK_AS_REVIEWED,
                    self::ACTION_MARK_AS_NOT_REVIEWED,
                ],
                'label' => false,
                'expanded' => true,
                'multiple' => false,
                'constraints' => [
                    new NotBlank(),
                ],
            ])
            ->add('comment',null)
            ->getForm();
Reply

Hey Peter K.!

Ooh, good question :). The 2nd option to createFormBuilder() are the options you're passing to the form. And in a form class, the configureOptions is how you pass option to the form. So basically, in addition to 'csrf_protection' => false, you should also pass a constraints key in that array, set to the big thing in the first example.

But let me know if it gives you any trouble ;).

Cheers!

1 Reply
Peter-K Avatar

Thanks working like a swiss watch

Reply
Polychronis Avatar
Polychronis Avatar Polychronis | posted 4 years ago | edited

Hi Ryan,

i am trying to add 1 more Unique User Validation rule for a username...

<u><i>src/Entity/User.php</i></u>`
/**

  • @ORM\Entity(repositoryClass="App\Repository\UserRepository")
  • @UniqueEntity(fields={"email"}, groups={"registration"}, message="This email is already registered!")
  • @UniqueEntity(fields={"username"}, groups={"registration"}, message="This username is already registered!")
    */
    `

<u><i>src/Form/Model/UserRegistrationFormModel.php</i></u>`

/**
 * @Assert\NotBlank(message="Please enter an email")
 * @Assert\Email()
 * @UniqueUser()
 */
public $email;

/**
 * @Assert\NotBlank(message="Please enter a username")
 * @UniqueUser()
 */
public $username;

`

Testing my Registration form, validation works for "<i>email</i>" but <b>NOT </b>for "<i>username</i>"
and what is funny :
i am still getting the "I think you're already registered!" message
although i changed it to "This email is already registered!"
even after <i>cache:clear</i>

any hints?

Reply

Hey Polychronis

Where does the @UniqueUser constraint comes from? Anyways, you only need to define the UniqueEntity constraint at the top of your class and that's it. Try removing that @UniqueUser from each property and remove the cache manually (just in case) rm -rf var/cache/*

Cheers!

1 Reply
Polychronis Avatar

Thanks for the hint !
Got confused between
- validating a form tied to an entity
- validating a form tied to a model

1 Reply
Markus B. Avatar
Markus B. Avatar Markus B. | posted 4 years ago | edited

I am searching for an example how a user can update his password, with two password fields which have to be equal an the user has to confirm the change with his old password. This is my form builder:

`

    $builder
        ->add('plainNewPasswordFirst', PasswordType::class, [
            'mapped' => false,
            'required' => false
        ])
        ->add('plainNewPasswordSecond', PasswordType::class, [
            'mapped' => false,
            'required' => false
        ])
        ->add('plainOldPassword', PasswordType::class, [
            'mapped' => false,
            'required' => false
        ])
    ;

`

Reply

Hey Markus B.!

Ah, excellent question! There are a few parts to the answer:

1) For the two password fields, use the RepeatedType. That is a built-in Symfony type that allows you to have "one field" that is actually two fields, and they must match each other else they will fail validation automatically. Super handy. We have an older (Symfony 3) tutorial that shows this - https://symfonycasts.com/screencast/symfony3-security/user-registration-form

2) For validating that the old password is "correct", you could go the "cheap" way and do this logic in your controller after submit (the downside being that you'd need to manually send some error string to the template and render it). Or, you could add a constraints option to plainOldPassword. You'll need to create a custom validation constraint (use bin/console make:validator) and point the constraint at it - e.g.


->add('plainOldPassword', PasswordType::class, [
    'mapped' => false,
    'required' => false,
    'constraints' => [new MatchesPassword()]
])

Then, in your MatchesPasswordValidator class, you can autowire the UserPasswordEncoderInterface service and use its isPasswordValid() method to check. You'll need to get current User object for this. It'll look something like this:


use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class MatchesPasswordValidator extends ConstraintValidator
{
    private $security;
    private $passwordEncoder;

    public function __construct(Security $security, UserPasswordEncoderInterface $passwordEncoder)
    {
        $this->security = $security;
        $this->passwordEncoder = $passwordEncoder;
    }

    public function validate($value, Constraint $constraint)
    {
        if (null === $value || '' === $value) {
            return;
        }

        $user = $this->security->getUser();
        if (!$this->passwordEncoder->isPasswordValid($user, $value)) {
            $this->context->buildViolation('Password does not match')
                ->addViolation();
        }
    }
}

So, there are a few steps involved :). Let us know if this helps!

Cheers!

Reply
Peter-K Avatar
Peter-K Avatar Peter-K | posted 4 years ago

Hi Ryan,

I have 2 questions:
First one is regarding custom validation. I would like to have a password that contains at least 8 characters and then at least 1 upper, 1 lower, 1 number & 1 special character? How would I do this? I believe I need some sort of my own validator.

Second question is controlling some Validation Annotation in entity by software settings(.env) for example.
Lets say at the moment application supports maximum 8 characters. So I have in my entity NotBlank max 8 but now other client decided to support 9 characters. Obviously I dont want to create another version where the only difference will be NotBlank max 9 Annotation.

Would you recommend in this case to not use Annotation and define validation rules directly in FormType class? Is there a way to have variables in annotation. Lets say NotBlank(min = $myMinVariableFromDotEnv)?

Thanks

Reply

Hey Peter K.!

> First one is regarding custom validation. I would like to have a password that contains at least 8 characters and then at least 1 upper, 1 lower, 1 number & 1 special character? How would I do this? I believe I need some sort of my own validator.

I would either use the Regex constraint (https://symfony.com/doc/cur... or the Callback constraint... if the Regex is too ugly :).

> Second question is controlling some Validation Annotation in entity by software settings(.env) for example.

THIS *would* require a custom validation constraint (which we do talk about later in this tutorial). With a custom validation constraint, you have a "validator", which is a service. Because of that, we can use dependency injection to "inject" your custom rules, for example, from .env. This would mean, for example, that you would have some new @Password annotation, which would ultimately call your custom validation constraint, where you could apply the dynamic logic.

Let me know if this all makes sense! Cool questions!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.6
        "symfony/console": "^4.0", // v4.1.6
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.6
        "symfony/framework-bundle": "^4.0", // v4.1.6
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.6
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.6
        "symfony/validator": "^4.0", // v4.1.6
        "symfony/web-server-bundle": "^4.0", // v4.1.6
        "symfony/yaml": "^4.0", // v4.1.6
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.6
        "symfony/dotenv": "^4.0", // v4.1.6
        "symfony/maker-bundle": "^1.0", // v1.8.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.6
    }
}
userVoice