If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeUnfortunately, you can't use the @UniqueEntity()
validation constraint above a class that is not an entity: it's just a known limitation. But, fortunately, this gives us the perfect excuse to create a custom validation constraint! Woo!
When you can't find a built-in validation constraint that does what you need, the next thing to try is the @Assert\Callback
constraint. We use this in the Article
class. But, it has one limitation: because the method lives inside an entity class - we do not have access to any services. In our case, in order to know whether or not the email
is taken yet, we need to make a query and so we do need to access a service.
When that's your situation, it's time for a custom validation constraint. They're awesome anyways and we're going to cheat! Find your terminal and run:
php bin/console make:validator
Call the class, how about, UniqueUser
. Oh, this created two classes: UniqueUser
and UniqueUserValidator
. You'll find these inside a new Validator/
directory. Look at UniqueUser
first: it's basically a dumb configuration object. This will be the class we use for our annotation.
... lines 1 - 6 | |
/** | |
* @Annotation | |
*/ | |
class UniqueUser extends Constraint | |
{ | |
/* | |
* Any public properties become valid options for the annotation. | |
* Then, use these in your validator class. | |
*/ | |
public $message = 'The value "{{ value }}" is not valid.'; | |
} |
The actual validation is handled by UniqueUserValidator
: Symfony will pass it the value being validated and a Constraint
object - which will be that UniqueUser
object we just saw. We'll use it to read some options to help us get our job done. For example, in the generated code, it reads the message
property from the $constraint
and sets that as the validation error. That's literally reading this public $message
property from UniqueUser
.
... lines 1 - 7 | |
class UniqueUserValidator extends ConstraintValidator | |
{ | |
public function validate($value, Constraint $constraint) | |
{ | |
/* @var $constraint App\Validator\UniqueUser */ | |
$this->context->buildViolation($constraint->message) | |
->setParameter('{{ value }}', $value) | |
->addViolation(); | |
} | |
} |
Ok: let's bring this generated code to life! Step 1: make sure your annotation class - UniqueUser
- is ready to go. In general, an annotation can either be added above a class or above a property. Well, you can also add annotations above methods - that works pretty similar to properties.
If you add a validation annotation above your class, then during validation, the value that's passed to that validator is the entire object. If you add it above a property, then the value that's passed is just that property's value. So, if you need access to multiple fields on an object for validation, then you'll need to create an annotation that can be used above the class. In this situation, I'm going to delete @UniqueEntity
and, instead, add the new annotation above my $email
property: @UniqueUser
. Hit tab to auto-complete that and get the use
statement.
... lines 1 - 4 | |
use App\Validator\UniqueUser; | |
... lines 6 - 7 | |
class UserRegistrationFormModel | |
{ | |
/** | |
... lines 11 - 12 | |
* @UniqueUser() | |
*/ | |
public $email; | |
... lines 16 - 26 | |
} |
Nice! Now, go back to your annotation class, we need to do a bit more work. To follow an example, press shift+ shift and open the core NotBlank
annotation class. See that @Target()
annotation above the class? This is a special annotation... that configures, um, the annotation system! @Target
tells the annotation system where your annotation is allowed to be used. Copy that and paste it above our class. This says that it's okay for this annotation to be used above a property, above a method or even inside of another annotation... which is a bit more of a complex case, but we'll leave it.
... lines 1 - 6 | |
/** | |
... line 8 | |
* @Target({"PROPERTY", "ANNOTATION"}) | |
*/ | |
class UniqueUser extends Constraint | |
... lines 12 - 19 |
What if you instead want your annotation to be put above a class? Open the UniqueEntity
class as an example. Yep, you would use the CLASS
target. The other thing you would need to do is override the getTargets()
method. Wait, why is there an @Target
annotation and a getTargets()
method - isn't that redundant? Basically, yep! These provide more or less the same info to two different systems: the annotation system and the validation system. The getTargets()
method defaults to PROPERTY
- so you only need to override it if your annotation should be applied to a class.
Phew! The last thing we need to do inside of UniqueUser
is give it a better default $message
: we'll set it to the same thing that we have above our User
class: I think you've already registered
. Paste that and... cool!
... lines 1 - 10 | |
class UniqueUser extends Constraint | |
{ | |
... lines 13 - 16 | |
public $message = 'I think you\'re already registered!'; | |
} |
If you need to be able to configure more things on your annotation - just create more public properties on UniqueUser
. Any properties on this class can be set or overridden as options when using the annotation. In UserRegistrationFormModel
, I won't do it now, but we could add a message=
option: that string would ultimately be set on the message
property.
Before we try this, go to UniqueUserValidator
. See the setParameter()
line? The makes it possible to add wildcards to your message - like:
The email {{ value }} is already registered
We could keep that, but since I'm not going to use it, I'll remove it. And... cool! With this setup, when we submit, this validator will be called and it will always fail. That's a good start. Let's try it!
Move over and refresh to resubmit the form. Yes! Our validator is working... it just doesn't have any logic yet! This is the easy part! Let's think about it: we need to make a query from inside the validator. Fortunately, these validator classes are services. And so, we can use our favorite trick: dependency injection!
Add an __construct()
method on top with a UserRepository $userRepository
argument. I'll hit alt+Enter to create that property and set it. Below, let's say $existingUser = $this->userRepository->findOneBy()
to query for an email set to $value
. Remember: because we put the annotation above the email
property, $value
will be that property's value.
Next, very simply, if (!$existingUser)
then return
. That's it.
... lines 1 - 8 | |
class UniqueUserValidator extends ConstraintValidator | |
{ | |
private $userRepository; | |
public function __construct(UserRepository $userRepository) | |
{ | |
$this->userRepository = $userRepository; | |
} | |
... line 17 | |
public function validate($value, Constraint $constraint) | |
{ | |
$existingUser = $this->userRepository->findOneBy([ | |
'email' => $value | |
]); | |
if (!$existingUser) { | |
return; | |
} | |
... lines 27 - 31 | |
} | |
} |
One note: if this were an edit form where a user could change their email, this validator would need to make sure that the existing user wasn't actually just this user, if they submitted without changing their email. In that case, we would need $value
to be the entire object so that we could use the id
to be sure of this. To do that, you would need to change UniqueUser
so that it lives above the class, instead of the property. You would also need to add an id
property to UserRegistrationFormModel
.
But, for us, this is it! Move back over, refresh and... got it! Try entering a new user and adding the novalidate
attribute so we can be lazy and keep the other fields blank. Submit! Error gone. Try WillRyker@theenterprise.org
with the same novalidate
trick. And... the error is back.
Custom validation constraints, check! Next, we're going to update our Article form to add a few new drop-down select fields, but... with a catch: when the user selects an option from the first drop-down, the options of the second drop-down will need to update dynamically. Woh.
Can I use it in buildForm() instead of annotation?
Like:
$builder->add('field', CustomValidator::class);
Hey Sergei,
Yes, you can! See "constraints" field option: https://symfony.com/doc/cur... - it's an array of your constraints you want to apply to the form field. But keep in mind that this will work only for the form where you put that constraint. E.g. if you need the same validation in another form - you will need to add that constraint manually. But if you put that constraint on the entity - it will be applied globally in all forms.
Cheers!
I Just have 1 form and I want manually to validate a data.
I want to validate the password repeat but completely stuck with that.
I send on server two fields:
password
repeated_password```
And in created form:
class ApiUserSignUpForm extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => UserModel::class,
'csrf_token_id' => 'head',
]);
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('repeated_password', RepeatedType::class, [
'first_name' => 'password',
'second_name' => 'repeated_password',
])
->add('password', PasswordType::class)
;
}
}
Try to validate it.
Validate is false with no errors.
Is it possible to check passwords, equals or not. Or I need to create custom valdiator with <b>return $password === $repeated_password</b>
Hey WebAdequate,
Did you dump that "repeated_password" value you get? IIRC it should be an array, something like: $data['repeated_password']['first'] and $data['repeated_password']['second'].
Btw, "repeated_password" already will give you 2 fields as you use RepeatedType::class type, see docs about it: https://symfony.com/doc/cur... . So, it sounds like you don't that extra "password" type. Or do you want to check that user enter his current password correctly before change his password to a new one from "repeated_password" field value?
Cheers!
Aha I didn't know it is a multiarray.
`->add('pwd_fieldname', RepeatedType::class, [
'first_name' => 'name1',
'second_name' => 'name2',
])```
$data['pwd_fieldname']['name1'] === $data['pwd_fieldname']['name2']` ?
Hey WebAdequate,
Yeah, it should be an array of nested arrays. Use "dd($data)" or "var_dump($data); die;" to see the actual structure of the form data and its array keys.
Cheers!
Thanks a lot for the answer! It works now.
Actually var_dump of <b>$form </b>is huge. It was difficult to explore.
So as I found out, I can't just use <b>$form->getErrors()</b> for rendering an erros. It always return for me 0.
But with <b>$form->getErrors(true)</b> I can iterate it. Like below:
Controller:
public function register(Request $request, JsonEncoder $encoder)
{
$raw = $request->getContent();
$data = $encoder->decode($raw, JsonEncoder::FORMAT);
$form = $this->createForm(ApiUserSignUpForm::class, new UserModel());
$form->submit($data['params']);
$form->isValid();
$errors = [];
if(!$form->isValid()) {
foreach ($form->getErrors(true) as $error) {
$errors[] = $error->getMessage();
}
return $this->json([
'success' => false,
'errors' => $errors
]);
}
$savedUser = $this->userService->signUpFromApi($form->getData());
return $this->json([
'success' => true,
'errors' => [],
'username' => $savedUser->getUsername(),
]);
}
Can we just receive simple array with <b>['username' => ['Username is too short!']]</b> without this ugly iteration in the controller?
Hey WebAdequate,
Yay, good work!
Yes, vardumping it might be a problem as it's a big object that contains other objects. I'd recommend you to user dd() or dump(); die; instead - see Symfony's VarDumper component - it should handle it well.
About $form->getErrors(true) - yes, it will give you all the errors including errors of child forms.
> Can we just receive simple array with ['username' => ['Username is too short!']] without this ugly iteration in the controller?
Nothing much we can do here as it's an object that contains other objects. You can write a custom reusable code that will do what you want and just call one method. Well, you can try to serialize errors with a serializer, but I'm not sure it will give you the structure you need out of the box, so in any case you will have to add some custom code for this.
P.S. And looks like you call isValid() twice, it's redundant.
Cheers!
So i found this solution:
`
$errorArray = [];
$errors = $form->getErrors(true, true);
foreach($errors as $e){
$field = $e->getOrigin();
$errorArray[$field->getName()] = $errorArray[$field->getName()] ?? [];
$errorArray[$field->getName()][] = $e->getMessage();
}
`
Looks absolutely ugly and isn't native but it works fine.
Hey triemli!
Yep, this is a pretty manual way to get a flat array of all of the errors in the form - it's ugly as you said, but I have done this in the past in a few situations.
Can I ask why you're needing to flatten the array of errors? Are you returning the errors as JSON instead of re-rendering the form? If so, you might be better-served by using the validator directly and then serializing it with the serializer component (Symfony's serializer service knows how to nicely serialize the result of the validation component (validation error).
Cheers!
Hi weaverryan this is solution for Vue application, because Vue works with JSON we sand a JSON for the form's errors render ;]
Hey triemli !
Ah, got it! So... you can actually just use the serializer :). Well, sort of. In Symfony 5.2 (not released yet), the serializer will have a "form normalizer" so that you can basically just do this:
return $this->json($form);
That's it! That will (in Symfony 5.2) automatically convert the errors on your form into a nice JSON response.
Since 5.2 isn't released yet, if you'd like, you could just move the new normalizer into your code: https://github.com/symfony/symfony/blob/5.x/src/Symfony/Component/Serializer/Normalizer/FormErrorNormalizer.php
It should be as easy as "copy into your src/ directory" and then "update the namespace". Oh, and just in case, don't forget to install the serializer if you don't have it already - "composer require serializer".
Cheers!
I just have a restriction on password field "length > 6". So basically I send password less than 6 symbols, validation is false (obviously) but $form->getErrors() is empty. $form['password']->getErrors() is empty too.
Sorry for my late reply! Hmm, that's very strange. If you open up the web profiler and go to the "forms" tab, what do you see after submitting an invalid form?
Hi everybody. I'm struggling with that story about the models.
Since the registration form, I have issues:<b> Cannot access private property App\Entity\User::$email</b>
So here is the incriminated part of the register function:
`
/** @var FormInterface $form */
$form = $this->createForm(UserRegistrationFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var UserRegistrationFormModel $userModel */
$userModel = $form->getData();
/** @var User $user */
$user = new User();
$user->setEmail($userModel->email);
$user->setRoles(['ROLE_USER']);
$user->setPassword($passwordEncoder->encodePassword(
$user,
$userModel->plainPassword
));
if (true === $userModel->agreeTerms) {
$user->agreeTerms();
}
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
}
`
The error is located at $user->setEmail($userModel->email);
My UserRegistrationFormModel:
`
<?php
class UserRegistrationFormModel
{
/**
* @Assert\NotBlank(message="Please, enter an email")
* @Assert\Email()
*/
public $email;
/**
* @Assert\NotBlank(message="Please, choose a password")
* @Assert\Length(min=5, minMessage="The password needs at least 5 chars. ")
*/
public $plainPassword;
/**
* @Assert\IsTrue(message="You have to agree to our terms")
*/
public $agreeTerms;
}
`
Help could be welcome, because I understand the meaning of this error but...
Hey Christina V.
I think you just forgot to make public the email
field on your UserModel class
Cheers!
Hi. In your UniqueUserValidator, is it possible to create another function of validation and call out ?
Hello!
I have 2 fields on my form : phone and mobile. At least one of them should be filled in. I guess that thee client side validation can only be done through javascript, right? But for the server side validation, is a custom validator (a class constraint) the right solution?
Thx!
Hey Lydie,
> I guess that thee client side validation can only be done through javascript
Do you mean 3 validations like make sure that phone is valid if specified (1), mobile is valid if specified (2), and at least one field is filled in (3)? Then this sounds good for me. But you can (should) do the same for server side validation as well.
For server side validation - yes, you can use a custom validator, or you can use Callback constraint as an alternative solution here: https://symfony.com/doc/cur... - it might be easier.
Cheers!
Hello victor !
Thx for your suggestion! I have implemented the callback method to test if at least one field is filled in:
/**
* @Assert\Callback()
*/
public function validate(ExecutionContextInterface $context, $payload)
{
if ((empty($this->getMobile()) && empty($this->getFixedPhone()))) {
$context->buildViolation('validation.phones')
->setTranslationDomain('prospects')
->atPath('mobile')
->addViolation();
}
Thx!
Hello!
Still have one open question for this callback. The validation is based on 2 fields of my form: mobile number and fixed phone number. The validation error message is attached to the mobile field and so it's displayed on top of this field. Would it be possible to specify a different location (for example a div id) ? Hope my question is clear enough :)
Hey Lydie,
Good question! You need to attach this validation constraint to the entire form instead of the specific field. IIRC, you just do not need that "->atPath('mobile')" because it attaches the message exactly to "mobile" field, but you most probably want to have this error message on an entire form as it's related to a few fields, or correctly say it relates to the entire form.
I hope this helps!
Cheers!
Thx for your answer, Victor ! We only have 2 possibilities: entire form or specific field? No way to display the error in the middle of the form ? For example, if you have your 2 fields next to each other (so on the same line), you will want to have the error on top of the line containing the 2 fields (to keep a nice layout)
Thx!
Hey Lydie,
Yes, only 2 options, display a form error or field specific error. But you can go wild and render the form your self ;) See this article in docs: https://symfony.com/doc/cur... - with {{ form_errors(form.fieldName) }} you can render specific form field error wherever you want :)
Cheers!
I followed the instructions to validate email on edit form. It works but it shows validator message on the top of the form. How can I change back the error's origin to email field?
Hey @Çağlar
That's a bit weird. Did you activate the "error_bubling" property on your form field?
Cheers!
The other validations' origin is their fields, I did not activate error_bubling. I think it is not weird, it just does not know on which field it needs to show error. I tried to imitate "UniqueEntity" to show it field but I could not make it work.
<b>UniqueUser:</b>
`/**
@Target({"CLASS", "ANNOTATION"})
*/
class UniqueUser extends Constraint
{
public $fields = [];
public function getRequiredOptions()
{
return ['fields'];
}
public function getDefaultOption()
{
return 'fields';
}
/**
/*
<b>UserFormModel:</b>
`/**
)
*/
class UserFormModel
{
public $id;
/**
/**
<b>UniqueUserValidator:</b>
`class UniqueUserValidator extends ConstraintValidator
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function validate($value, Constraint $constraint)
{
$existingUser = $this->userRepository->findOneByEmail($value->email, $value->id);
if (!$existingUser) {
return;
}
/* @var $constraint \App\Validator\UniqueUser */
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $value->email)
->addViolation();
}
}`
Ohh, you created a Class constraint instead of a Property constraint, that's why the error appears as a global error, you can manipulate the error path by defining a public property "errorPath" on your constraint class. Then just set that value on your annotations:
/**
* @UniqueUser(
* fields={"email"}
* errorPath="email"
* )
*/
class UserFormModel
Thanks for helps. I also called atPath function in "UniqueUserValidator" to make it work:
`
$this->context->buildViolation($constraint->message)
->atPath($constraint->errorPath)
->setParameter('{{ value }}', $value->email)
->addViolation();
`
I found out, that you don't need to set the errorPath explicitly.
This is dynamic:
$errorPath = null !== $constraint->errorPath ? $constraint->errorPath : $fields[0];
$this->context->buildViolation($constraint->message)
->atPath($errorPath)
->setParameter('{{ value }}', $value->$fieldName)
->addViolation();```
Btw. thanks SymfonyCast Team for your outstanding work!
My email & password validation error messages aren't showing anymore :( They are visible in the Profiler.validator calls but not being written to the page... Any ideas where I've went wrong?
Hey Paul S.
How did you build your login form? If you did it manually, then you will have to print those errors manually as well
Cheers!
Hello, I need the answer to this very question : I'm facing this problem too, annotation is above a class, the invalid value if an object instead of the say email,
` $this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $value->getEmail())
->addViolation();`
with this the display message can be viewed in the but it is not showing on the form, care to tell me how to do it "manually" thanks
Ok I just figured out, you don't need to alter the entity, just have to add this line
` $this->context->buildViolation($constraint->message)
->atPath('clientTelFixe'). // this line is important
->setParameter('{{ value }}', $value->getClientTelFixe())
->addViolation();`
See <a href="#">https://symfony.com/doc/current/validation/custom_constraint.html</a>
Hi, on video you said:
But, it has one limitation: because the method lives inside an entity class - we do not have access to any services. In our case, in order to know whether or not the email is taken yet, we need to make a query and so we do need to access a service.
..but our UserRegistrationFormModel isn't entity ;] and we can use autowiring ;]
Hey Kris!
Ah, very clever of you! But... nope! The reason an entity is not a service is not *actually* because it's an entity.. and so that makes it special somehow. The key thing is that an "entity" is something that we do NOT want handled by the container. Why? Because the container will only ever allow a single instance of an object. And so, these "data-holding" object don't fit well int that model. For example, if I want to display 10 products on a page, it TOTALLY makes sense to have 10 Product entity objects.
Let me say it a different way. If you code cleanly, you will have 2 different types of classes/objects
A) Simple model objects: objects that hold data but don't really do much work. It makes sense to have multiple instances of these objects at any given time. An entity is an example of this, but so UserRegistrationFormModel
B) Service objects: objects that do not hold much data (maybe just some config) and primarily do work. An important property of these is that it only makes sense to have ONE instance of these classes ever. For example, think of some "mailer" object. If you need to send 5 emails, do you need 5 Mailer instances? Nope - just 1 - and you would call some "sendEmail" message on it 5 times (just an example). THESE are the objects that the container is meant to instantiate.
Now, for your specific situation :). Do to how "friendly" Symfony's config is, you technically CAN use UserRegistrationFormModel as a service - Symfony just sees this as a class, and so if you, for example, add UserRegistrationForModel as an argument to a controller, Symfony will say "Oh, that must be a service, let's instantiate it, autowire all its arguments, and pass it in". But, this is improper use, and it'll bite you eventually. Most importantly, it starts to "blur" that clean distinction between model classes and service classes.
Let me know if that makes sense. What you're thinking is not surprising - I was actually wondering if this aspect would begin to confuse people :).
Cheers!
// 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
}
}
`$existingUser = $this->userRepository->count([
]);`
should be a bit faster than the course code as it avoids fetching the entire user object