Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Custom Validator

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

Here's the situation: all authenticated users should have access to create a CheeseListing... and one of the fields that can be passed is owner. But the data passed to the owner field may be valid or invalid depending on who you're authenticated as. For a normal user, I'm supposed to set this to my own IRI: if I try to set it to a different IRI, that should be denied. But for an admin user, they should be allowed to set the IRI to anyone.

When the value of a field may be allowed or not allowed based on who is authenticated, that should be protected via validation... which is why we're expecting a 400 status code - not a 403.

Generating the Custom Validator

Ok, so how can we make sure the owner field is set to the currently-authenticated user? Via a custom validator. Find your terminal and kick things off with:

php bin/console make:validator

Call it IsValidOwner. If you're not familiar with validators, each validation constraint consists of two classes - you can see them both inside the src/Validator/ directory. The first class represents the annotation that we will use to activate this... and it's usually empty, except for a few properties that are typically public. Each public property will become an option that you can pass to the annotation. More on that in a minute.

... lines 1 - 9
class IsValidOwner 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 other class, which typically has the same name plus the word "Validator", is what will be called to do the actual work of validation. The validation system will pass us the $value that we're validating and then we can do whatever business logic we need to determine if it's valid or not. If the value is invalid, you can use this cool buildViolation() thing to set an error.

... lines 1 - 7
class IsValidOwnerValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
/* @var $constraint \App\Validator\IsValidOwner */
if (null === $value || '' === $value) {
return;
}
// TODO: implement the validation here
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $value)
->addViolation();
}
}

Using the Validation Constraint

To see this in practice, open up CheeseListing. The property that we need to validate is $owner: we want to make sure that it is set to a "valid" owner... based on whatever crazy logic we want. To activate the new validator, add @IsValidOwner(). This is where we could customize the message option... or any public properties we decide to put on the annotation class.

... lines 1 - 10
use App\Validator\IsValidOwner;
... lines 12 - 48
class CheeseListing
{
... lines 51 - 95
/**
... lines 97 - 99
* @IsValidOwner()
*/
private $owner;
... lines 103 - 206
}

Actually, let's change the default value for $message:

Cannot set owner to a different user

... lines 1 - 9
class IsValidOwner extends Constraint
{
... lines 12 - 15
public $message = 'Cannot set owner to a different user';
}

Ok, now that we've added this annotation, whenever the CheeseListing object is being validated, the validation system will now call validate() on IsValidOwnerValidator. The $value will be the value of the $owner property. So, a User object. It also passes us $constraint, which will be an instance of the IsValidOwner class where the public properties are populated with any options that we may have passed to the annotation.

Avoid Validating Empty Values

The first thing the validator does is interesting... it checks to see if the $value is, sort of, empty - if it's null. If it is null, instead of adding a validation error, it does the opposite! It returns.. which means that, as far as this validator is concerned, the value is valid. Why? The philosophy is that, if you want this field to be required, you should add an additional annotation to the property - the @Assert\NotBlank constraint. We'll do that a bit later. That means that our validator only has to do its job if there is a value set.

The setParameter() Wildcard

To see if this is working... ah, let's just try it! Sure, we haven't added any logic yet... and so this constraint will always have an error... but let's make sure we at least see that error!

Oh, and this setParameter() thing is a way for you to add "wildcards" to the message. Like, if you set {{ value }} to the email of the User object, you could reference that dynamically in your message with that same {{ value }}. We don't need that... so let's remove it. Oh, and just to be totally clear, the $constraint->message part is referencing the $message property on the annotation class. So, we should see our customized error.

Let's try it! Go tests go!

php bin/phpunit --filter=testCreateCheeseListing

If we scroll up... awesome! It's failing: it's getting back a 400 bad request with:

Cannot set owner to different user

Hey! That's our validation message! The failure comes from CheeseListingResourceTest line 44. Once we use a valid owner IRI, validation still fails because... of course... right now our new validator always adds a violation.

Let's fix that next: let's add real logic to make sure the $owner is set to the currently-authenticated user. Then we'll go further and allow admin users to set the $owner to anyone.

Leave a comment!

20
Login or Register to join the conversation
Alberto rafael P. Avatar
Alberto rafael P. Avatar Alberto rafael P. | posted 3 years ago

Tengo muchas ganas de ver este capítulo!!!

1 Reply

Proximamente! Este curso esta siendo publicado diariamente

Saludos!

Reply
Lechu85 Avatar

Hello.
it wouldn't be more convenient and safer to use instead of:

I if (null === $value || '' === $value) { ... }

this code:

I if (!empty($value)) { ... }
? :)

Reply

Hey Leszek C.

That's a good question. In theory, your solution looks better but it works a bit different, for instance, it will allow arrays to be passed in. In this case, Ryan it's been very explicit with what exact values he want to skip

Cheers!

1 Reply
Christian H. Avatar
Christian H. Avatar Christian H. | posted 1 year ago

The validator is not API platform specific, right?

I have a strange problem: a validator that always shows a violation test-wise.

This is triggered, as expected, on an API call. The same via EasyAdmin.

But not when I create a new object and write it to the database using Doctrine Eventmanager. Then the validator is not called.

Is that the way it is supposed to be? Or do I have something configured wrong?

I use Symfony 5.2. and PHP 8.0.3

Reply

Hey Christian H.

Yes, the validator is its own component the Symfony Validator. ApiPlatform has it already integrated, the same thing with EasyAdmin but in your Symfony app, if you are not relying on a Symfony Form, then you have to manually use the validator service. It's quite simple to use, here you can read more about it https://symfony.com/doc/cur...

Feel free to ask us any doubts about it. Cheers!

Reply
Christian H. Avatar
Christian H. Avatar Christian H. | MolloKhan | posted 1 year ago

OK. I got it! Thanks!

What's the best practice? Call manually every time before a "persist"?

Or can this be done automatically (e.g. via lifecycle callbacks)? But then a API call would be validated twice (by Doctrine and API Platform, right?).

Reply

If you have many places where you're creating those objects, it may makes sense to create a service class that you can use everywhere and to it the object's data, so it can instantiate the object, use the validator service, and all that stuff.

Cheers!

Reply

I am writing my first custom validator and have hit a block. What I am trying to accomplish is as follows. I have an entity that contains a list of "courses" that the student has signed up for and another entity that has the students "grades". The reason that they are in separate entities is that the student can attempt an examination more than once for a maximum of 3 times. I need to write a custom validator to and use it in the "grades" entity. the vlaidator should check if the student has signed up for the "course" and the maximum number of tries has not been exhausted that is it is < 3. Is there aq way to fetch the data from the entity in the validator?

Reply

Hey sridharpandu !

Hmm. So if you're inside a custom validator that is validating the Grade entity (and assuming you have added this new, custom validation constraint annotation above your Grade class), then you already have access to the Grade object. From there, I'm assuming that there is some method like $grade->getStudent() to get the Student object that this grade belongs to. And there must also be some sort of $grade->getCourse() so that you know what Course this grade is for.

If so, you could use that Student object and Course object to query the "list of courses" entity to see if you get any results. I would autowiring that entity's repository into the validator to do that. You could also query the "grades" table WHERE student = this student and course = this course to see how many results you get back.

Does that help? Or is there some unclear piece... or some details I'm missing?

Cheers!

1 Reply

Swagger throws an error when I autowire the entity class into the validator. The error is :

Cannot autowire service "App\Validator\gradeValidator"; argument "$course" of method "__construct()" has type "App\Validator\Courses" but this class is not found.

My validator is a s follows

gradeValidator.php
-------------------------
namespace App\Validator;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class gradeValidator extends ConstraintValidator
private $courses;

public function __construct(Courses $course)
{
$this->course = $course);
}

public function validate(4value, Constraint $constraint)
{
........................
}
}

Traced back all lessons in Symfonycasts only to realize that entity repository autowiring was not covered. Then got a few posts on StackOverflow that discussed this problem on an older symfony version. Based on these suggestion I included the following use statements

use Doctrine\ORM\ServiceEntityRepository;
use Doctrine\ORM\ManagerRegistry;

but the error persisted. So read through few more Stackoverflow posts and one suggested to include the following instead of the two statements above.
use Doctrine\ORM\EntityManagerInterface;

But including these statements did not help. Any suggestions on what should I include in the validator so that the entity gets autowired.

Thanks in advance.

Reply

Hi sridharpandu!

Hmm. Ok, a few things :). Entity repositories are normal services. So you can autowire them in the same way as any other service. Don't forget that you can run bin/console debug:autowiring --all to see a full list of classes and interfaces that can be used for autowiring.

If your validator itself, what you want to autowire isn't a Courses class but something called CoursesRepository or CourseRepository. If you used make:entity to originally generate your entity, then this should have been created for you automatically in the src/Repository directory. THAT is service class you want to autowire.

Once you have access to it, you can create custom methods that make custom queries. Here is a video all about the repository topic - https://symfonycasts.com/sc... - the only difference in that (other than we call our repository from a controller instead of from a validator service) is that we autowire EntityManagerInterface and then use its ->getRepository() method to get the repository. Both actually work - you could autowire EntityManagerInterface into your constructor or (do the easier route) where you just autowire the CourseRepository directly. We change to autowire the repository directly later - you can see that in the next video - https://symfonycasts.com/sc...

Cheers!

1 Reply

Thanks a ton. I got a bit confused with your previous answer being a bit different from the tutorail 12 - Custom Repository class. So was trying to autowire the entity. I have now managed to autowire the repository successfully except that Symfony 5.2 expects a use statement,

use App\Repository\CoursesRepository;

which is not visible in your videos. Is this expected behaviour?

In the meantime trying my hand at writing custom queries.

EDIT 1
----------
Is there a way to refer to the data being passed to the entity that has triggered this validator? Does the $value argument of the validate function contain the object/data? A dd doesn't do anything on the API Platform. I want to accomplish a equi join between two entities so thge correct record is retrieved from the Database. In the video lessons one side of the argument passed is always a literal.

EDIT 2
---------
The use case I described in my post can be solved on the GUI by incorporating a drop down list of courses where the number of tries <3 (implemented on API platform using a filter) so that the user can only create grades where the number of attempts is less than there but then this would mean delegating functional responsibility to the UI which is something that I would like to avoid by retaining all functional checks in the API.

Reply

Hey sridharpandu!

> I have now managed to autowire the repository successfully except that Symfony 5.2 expects a use statement... which is not visible in your videos. Is this expected behaviour?

Yes, good catch! And sorry about this. When you allow a class name to be auto-completed in PhpStorm, it automatically adds that use statement to the top of the class. So it IS being added when I code, but you can't see it. This does confuse some people (and I totally understand why), but if I scrolled up on every "use" statement to show it, it would be super annoying. So there is no perfect answer to this :). Btw, you can always check the code blocks on the page and expand them to see the full code for a spot.

> Is there a way to refer to the data being passed to the entity that has triggered this validator?
> Does the $value argument of the validate function contain the object/data? A dd doesn't do anything on the API Platform.

I'm surprised a dd() doesn't trigger anything: that *should*. As long as you have the validation annotation above a property or class (more info about this below), that should trigger the validate() method to be called.

The answer to "what" the $value argument will be depends *where* you put the annotation. For example, in this chapter, I put the @IsValidOwner annotation above the owner property. And so, I am passed THAT value. In this case, it means I am passed a User object. If I put a custom validation annotation above a "string" property, then I would be passed that string. You can *also* put an annotation above an entire *entity* - above the class. And in that case, you are passed the entire *object*. For example, if you put your annotation above the Course class, then the $value would be the Course argument. We have an example of this type of validator here: https://symfonycasts.com/sc...

Cheers!

1 Reply

Using the annotation at the attribute level triggers the validation.

I wasn't very explicit when I said that a dd() doesnt trigger anything, I used dd() to dump the variables especially the $value but didn't see anything on the swagger UI nor in the symfony profiler. But will check again to see if its hidden.

Thanks a lot for the detailed explanation on the contents of $value , it solves a lot of functional issues that I would encounter if didn't contain the value of the property or entity.

Reply

Hey sridharpandu!

> I used dd() to dump the variables especially the $value but didn't see anything on the swagger UI nor in the symfony profiler

Hmm. If you use dd(), it should "kill" the AJAX call. So you would need to look at your browser's network tools to find that AJAX request and look at its response. If you switch that to dump() instead, then you should be able to use the web debug toolbar AJAX icon on the bottom of your screen to find that AJAX request, open it in the profiler, and see the dump (via the Debug section on the left).

Good lucks and cheers!

1 Reply

Finally managed to complete a custom validator.Since I had autowired a repository class had to first write a working findBy() function. The video tutorials used setParameter() as the query builder had a single parameter, I had two!, so had to work on setParameters(). It takes an array of parameters so had to cast the variables into an array before I could use it. The reason for using andWhere() instead of where() was very helpful otherwise would have spent some more time reading the doctrine docs. I noticed that access to the properties of the classes that are injected into the validator can be only obtained by using the get methods and not by reference. For example

$value_courseName = $value->Coursename //Not allowed

$value_courseName = $value->getCoursename() //Allowed

This is a fabulous implementation of object orientation and helps preserve scope and unnecessary manipulation of properties when an object's properties are passed by reference. There were several Front End Development tools in the late nineties that suffered from this drawback.

And finally the repository's findBy() method returns an array of objects, Accessing the right propery requires traversing this array with an off-set and accessing the objects get method like this

$courseName[0]->getCourseName()

Hope this hellps others understand the way Symfonyt is wired. Thanks a lot for answering my queries.

Reply

Hey sridharpandu!

Nice work!!! And I *really* appreciate your sharing your solution for others - we LOVE that here :).

Cheers!

1 Reply

Custom validators as a concept are awesome. It eases enterpr application development. Enterprise applications are a few forms with a zillion validations and processes. As a developer we can write once and use it anywhh. Simple validations like age, Social Security Number on application forms are being written several times withinin the same application it has been frustrating to identify these in code. Now I guess this structured approach will keep it clean and easy to maintain.

Reply

Hey Sridhar,

Agree, custom validators give you a lot of power and flexibility, so you literally can write any crazy business logic you need.

Cheers!

Reply
Cat in space

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

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3, <8.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.5
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.6
        "nesbot/carbon": "^2.17", // 2.21.3
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.3.*", // v4.3.2
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/expression-language": "4.3.*", // v4.3.2
        "symfony/flex": "^1.1", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/http-client": "4.3.*", // v4.3.3
        "symfony/monolog-bundle": "^3.4", // v3.4.0
        "symfony/security-bundle": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.6", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    },
    "require-dev": {
        "hautelook/alice-bundle": "^2.5", // 2.7.3
        "symfony/browser-kit": "4.3.*", // v4.3.3
        "symfony/css-selector": "4.3.*", // v4.3.3
        "symfony/maker-bundle": "^1.11", // v1.12.0
        "symfony/phpunit-bridge": "^4.3", // v4.3.3
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2
    }
}