Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Validation Groups

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

We're missing some validation related to the new password setup. If we send an empty POST request to /api/users, I get a 400 error because we're missing the email and username fields. But what I don't see is a validation error for the missing password!

No problem. We know that the password field in our API is actually the plainPassword property in User. Above this, add @Assert\NotBlank().

... lines 1 - 36
class User implements UserInterface
{
... lines 39 - 78
/**
... lines 80 - 81
* @Assert\NotBlank()
*/
private $plainPassword;
... lines 85 - 217
}

We're good! If we try that operation again... password is now required.

Sigh. But like many things in programming, fixing one problem... creates a new problem. This will also make the password field required when editing a user. Think about it: since the plainPassword field isn't persisted to the database, at the beginning of each request, after API Platform queries the database for the User, plainPassword will always be null. If an API client only sends the username field... because that's all they want to update... the plainPassword property will remain null and we'll get the validation error.

Testing the User Update

Before we fix this, let's add a quick test. In UserResourceTest, add a new public function testUpdateUser() with the usual $client = self::createClient() start. Then, create a user and login at the same time with $this->createUserAndLogin(). Pass that the $client and the normal cheeseplease@example.com with password foo.

... lines 1 - 7
class UserResourceTest extends CustomApiTestCase
{
... lines 10 - 27
public function testUpdateUser()
{
$client = self::createClient();
$user = $this->createUserAndLogIn($client, 'cheeseplease@example.com', 'foo');
... lines 32 - 41
}
}

Great! Let's see if we can update just the username: use $client->request() to make a PUT request to /api/users/ $user->getId(). For the json data, pass only username set to newusername.

... lines 1 - 27
public function testUpdateUser()
{
... lines 30 - 32
$client->request('PUT', '/api/users/'.$user->getId(), [
'json' => [
'username' => 'newusername'
]
]);
... lines 38 - 41
}

This should be a totally valid PUT request. To make sure it works, use $this->assertResponseIsSuccessful()... which is a nice assertion to make sure the response is any 200 level status code, like 200, 201, 204 or whatever.

And... to be extra cool, let's assert that the response does contain the updated username: we'll test that the field did update. For that, there's a really nice assertion: $this->assertJsonContains(). You can pass this any subset of fields you want to check. We want to assert that the json contains a username field set to newusername.

... lines 1 - 27
public function testUpdateUser()
{
... lines 30 - 37
$this->assertResponseIsSuccessful();
$this->assertJsonContains([
'username' => 'newusername'
]);
}

It's gorgeous! Copy the method name, find your terminal, and run:

php bin/phpunit --filter=testUpdateUser

And... it fails! 400 bad request because of the validation error on password.

Validation Groups

So... how do we fix this? We want this field to be required for the POST operation... but not for the PUT operation. The answer is validation groups. Check this out: every constraint has an option called groups. These are kinda like normalization groups: you just make up a name. Let's put this into a... I don't know... group called create.

... lines 1 - 39
class User implements UserInterface
... lines 41 - 81
/**
... lines 83 - 84
* @Assert\NotBlank(groups={"create"})
*/
private $plainPassword;
... lines 88 - 220
}

If you don't specify groups on a constraint, the validator automatically puts that constraint into a group called Default. And... by... default... the validator only executes constraints that are in this Default group.

We can see this. If you rerun the test now:

php bin/phpunit --filter=testUpdateUser

It passes! The NotBlank constraint above plainPassword is now only in a group called create. And because the validator only executes constraints in the Default group, it's not included. The NotBlank constraint is now never used.

Which... is not exactly what we want. We don't want it to be included on the PUT operation but we do want it to be included on the POST operation. Fortunately, we can specify validation groups on an operation-by-operation basis.

Let's break this access_control onto the next line for readability. Add a comma then say "validation_groups"={}. Inside, put Default then create.

... lines 1 - 16
/**
* @ApiResource(
... line 19
* collectionOperations={
... line 21
* "post"={
... line 23
* "validation_groups"={"Default", "create"}
* },
* },
... lines 27 - 33
* )
... lines 35 - 38
*/
class User implements UserInterface
... lines 41 - 222

The POST operation should execute all validation constraints in both the Default and create groups.

Find your terminal and, this time, run all the user tests:

php bin/phpunit tests/Functional/UserResourceTest.php

Green!

Next, sometimes, based on who is logged in, you might need to show additional fields or hide some fields. The same is true when creating or updating a resource: an admin user might have access to write a field that normal users can't.

Let's start getting this all set up!

Leave a comment!

22
Login or Register to join the conversation
Covi A. Avatar
collectionOperations: [
'get',
'post' => [
"security" => "is_granted('IS_AUTHENTICATED_ANONYMOUSLY')",
'validation_groups' => ['Default', 'create']
]
],

and

    #[SerializedName('password')]
/**
* @Assert\NotBlank(groups={"create"})
*/
private $plainPassword;

also add return type above getPlainPassword function

    /**
* @return string
*/
public function getPlainPassword(): string
{
return $this->plainPassword;
}


after setup this things it worked perfectly for create new post. i mean POST request. but when i try for PUT operation
got this error
TypeError : App\Entity\User::getPlainPassword(): Return value must be of type string, null returned

i spend so much time solving it. but i can't. could you please help me!

Reply

Hey Covi A.!

Ah, I see! Ok, because you're using return types on your methods (good job!), you need to change getPlainPasswords() return type to ?string - i.e. a nullable string. Think about it: when you run a PUT operation, API Platform/Doctrine queries the database for your User. That User object will have null for its plainPassword property, since that is not a persisted property. Then, something (I'm not sure exactly what, but the stack-trace would show you), something calls getPlainPassword() where you try to return null, but your return-type is string. Change it to ?string and you should be good :).

Cheers!

Reply
Covi A. Avatar

it works,
Thank you very much.

Reply
Sardar K. Avatar
Sardar K. Avatar Sardar K. | posted 11 months ago

Hi, when i try to modify with put request it returns 400 status code which is the expected response but the problem is that it modifies the database even if the response status code is 400 can you please help find the problem


public function testEditUser() {
$client = self::createClient();
$this->login($client, $this->email);
$currentUser = $this->getCurrentLoggedUser();
$id = $currentUser->getId();
$client->request("PUT", "/api/users/$id", [
'json' => [
"fullname" => "",
"gender" => "",
]
]);
$this->assertResponseStatusCodeSame(400);
}
Reply

Hey Sardar K.!

Ah! So, this means that - SOMEWHERE in your code (or potentially 3rd party code) you have something that, during the request, is running $entityManager->flush(). This is a weird, unfortunate thing with how Doctrine works. When you make the PUT request, you are modifying the User entity. That is no problem... unless someone calls $entityManager->flush(). If that happens, Doctrine will save ALL entities that it's aware of, which will include persisting your modified User object to the database.

See if you can track down where & why the ->flush() call is coming from.

Cheers!

Reply
Hazhir A. Avatar
Hazhir A. Avatar Hazhir A. | posted 1 year ago

hi, how can we use php8 syntax for validation groups

Reply
Hazhir A. Avatar
Hazhir A. Avatar Hazhir A. | posted 1 year ago

hi, how can we use php8 syntax for validation groups , is it possible?

Reply

Hey,

If you're on Symfony 5.2 I believe it should just work. Have you give it a try?
You can read a bit more about it here: https://symfony.com/blog/ne...}

Cheers!

Reply
Ben G. Avatar

Where the createUserAndLogin come from ?

Reply

Hey Ben,

It comes from the parent class, see CustomApiTestCase for more info. Btw, I'd recommend you to use PhpStorm, when you will hold Command button and press on the method name - it will open the method from parent class for you.

Cheers!

Reply
Ben G. Avatar

This is what i thought, I didn't found in which chapter we did it.

(I prefere do it all manually than copy paste things)

Reply
akincer Avatar
akincer Avatar akincer | posted 1 year ago

I'm not getting all green after defining the groups for the NotBlank and I'm still getting password should not be blank. I've copy and pasted all relevant code from the script to make sure I'm not making any typos. According to the error it's failing on the PUT.

1) App\Tests\Functional\UserResourceTest::testUpdateUser
Failed asserting that the Response is successful.
HTTP/1.1 400 Bad Request
Cache-Control: max-age=0, must-revalidate, private
Content-Type: application/ld+json; charset=utf-8
Date: Thu, 15 Oct 2020 23:42:43 GMT
Link: <http: example.com="" api="" docs.jsonld="">; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"
Set-Cookie: MOCKSESSID=ea6ddfddc68e01293646922b743207dd0f6c9a208b1a40be31044517388a83d9; path=/; secure; httponly; samesite=lax
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Robots-Tag: noindex

{"@context":"\/api\/contexts\/ConstraintViolationList","@type":"ConstraintViolationList","hydra:title":"An error occurred","hydra:description":"password: This value should not be blank.","violations":[{"propertyPath":"password","message":"This value should not be blank."}]}

C:\Users\ack\PhpstormProjects\API-Platform-Training2\src\ApiPlatform\Test\BrowserKitAssertionsTrait.php:42
C:\Users\ack\PhpstormProjects\API-Platform-Training2\tests\Functional\UserResourceTest.php:32

Reply

Hey @Aaron!

Hmmm. What happens if you *remove* the @Assert\NotBlank from the plainPassword field (and, you *do* have it on the plainPassword field, and not password, right?)? I'm curious to see if it saves correctly (meaning the field IS being set, but something is wrong with validation) or it explodes when trying to INSERT because the password field is blank.

Here are other things I would check:

A) Make sure the plainPassword is in the user:write group and has @SerializedName
B) Make sure there is a setPlainPassword() method (and double-check for any typos there).

Let me know what you find out! It sounds like probably a small missing detail somewhere. At the end of the day, this "plainPassword" field is a normal API field... and if it's blank (even though you're sending the "password" field), then something is slightly wrong. Oh, and feel free to post some code!

Cheers!

Reply
akincer Avatar

It worked removing it and then after adding it back it still worked. I can't understand how that works so I gotta assume I managed to make a typo even copying and pasting if that's even a thing which apparently it must be. Thanks!

Reply

Ha! Gremlins in your computer. I’m glad you got it working :).

Cheers!

Reply
akincer Avatar

Indeed. By the way -- it seems like there's a typo in the last command. Isn't the folder tests and not test? Somehow it seems to automagically work though.

Reply

Ah, good catch - you're totally right. Fixed! https://github.com/SymfonyC...

Thanks!

Reply
Hannah R. Avatar
Hannah R. Avatar Hannah R. | posted 2 years ago

After adding the validation_groups i get following deprecation notices and i dont really understand what they are about :)

3x: Not setting the "method" attribute is deprecated and will not be supported anymore in API Platform 3.0, set it for the collection operation "validation_groups" of the class "App\Entity\User".

1x: The "route_name" attribute will not be set automatically again in API Platform 3.0, set it for the collection operation "validation_groups" of the class "App\Entity\User".

Using Symfony 5 and Api Platform 2.5

Reply

Hey Hannah R.

Looks like there will be some upcoming changes in api platform configuration. There is nothing to worry about because when version 3.0 will be released all configuration changes should be listed. The weird situation that it's showing that validation_groups is a collection operation, did you put it in correct place? if so try to add "method"="POST" to see if it change errors

It should be like:

 *          "post"={
* "method"="POST",
* "access_control"="is_granted('IS_AUTHENTICATED_ANONYMOUSLY')",
* "validation_groups"={"Default", "create"}
* },

Cheers!

1 Reply
Qcho Avatar

Hi,

This video confirms something I've been thinking on every video of this course.
It seems you are mixing 'PATCH' with 'PUT' responsibilities.

'PUT' should have REQUIRED password the same as CREATE because it's a REPLACEMENT of the object a complete and valid object to replace.

Usually `PATCH` is used for partial modification of objects such as stated "if a user want only to modify it's username"

Am I missing something? I know this kind of details are usually messed up but this course seems to be so perfect and consistent with best-practices that I needed to ask

From : https://api-platform.com/do...
PUT: Replace an element.
PATCH: Apply a partial modification to an element.

Thanks for this great course!

Reply

Hey Qcho

What you stated is correct. Put is for updating a whole resource and Patch is for a partial update where you only send the info you want to update.
You may or may not want to send the password on a PUT request, it depends on your security policies.

Cheers!

1 Reply
Thibaut C. Avatar

If I understant well, you mean that 'password' si a special field, but apart from 'password', I understant from your answer, what is done in this video is not really standard, thus,you should either

- Make a PATCH request insteat of the PUT request
- Inculde all fields (maybee you can exclude password) in the PUT request

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
    }
}