Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: ^7.1.3, <8.0
Subscribe to download the code!Compatible PHP versions: ^7.1.3, <8.0
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Custom Validator
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeHere'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.
Show Lines
|
// ... 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.
Show Lines
|
// ... 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.
Show Lines
|
// ... lines 1 - 10 |
use App\Validator\IsValidOwner; | |
Show Lines
|
// ... lines 12 - 48 |
class CheeseListing | |
{ | |
Show Lines
|
// ... lines 51 - 95 |
/** | |
Show Lines
|
// ... lines 97 - 99 |
* @IsValidOwner() | |
*/ | |
private $owner; | |
Show Lines
|
// ... lines 103 - 206 |
} |
Actually, let's change the default value for $message
:
Cannot set owner to a different user
Show Lines
|
// ... lines 1 - 9 |
class IsValidOwner extends Constraint | |
{ | |
Show Lines
|
// ... 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.
20 Comments
Proximamente! Este curso esta siendo publicado diariamente
Saludos!
Hello.
it wouldn't be more convenient and safer to use instead of:
I if (null === $value || '' === $value) { ... }
this code:
I if (!empty($value)) { ... }
? :)
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!
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
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!
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?).
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!
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?
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!
Swagger throws an error when I autowire the entity class into the validator. The error is :
<blockquote>Cannot autowire service "App\Validator\gradeValidator"; argument "$course" of method "__construct()" has type "App\Validator\Courses" but this class is not found.</blockquote>
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;<br />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.
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/screencast/symfony-doctrine/more-queries - 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/screencast/symfony-doctrine/query-builder#autowiring-the-repository-directly
Cheers!
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.
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!
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.
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!
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
`<br />$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.
Hey sridharpandu!
Nice work!!! And I *really* appreciate your sharing your solution for others - we LOVE that here :).
Cheers!
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.
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!
"Houston: no signs of life"
Start the conversation!
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.21.6
"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
}
}
Tengo muchas ganas de ver este capÃtulo!!!