If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
The main HTTP methods are: GET, POST, PUT and DELETE. There's another one you hear a lot about: PATCH.
The simple, but not entirely accurate definition of PATCH is this: it's just
like PUT, except you don't need to send up the entire resource body. If you
just want to update tagLine
, just send that field.
So really, PATCH is a bit nicer to work with than PUT, and we'll support
both. Start with the test - public function testPATCHProgrammer()
:
... lines 1 - 93 | |
public function testPATCHProgrammer() | |
{ | |
... lines 96 - 111 | |
} | |
... lines 113 - 125 |
Copy the inside of the PUT test: they'll be almost identical.
If you follow the rules with PUT, then if you don't send tagLine
, the
server should nullify it. Symfony's form system works like that, so our PUT
is acting right. Good PUT!
But for PATCH, let's only send tagLine
with a value of bar
. When we
do this, we expect tagLine
to be bar
, but we also expect avatarNumber
is still equal to 5. We're not sending avatarNumber
, which means: don't
change it. And change the method from put()
to patch()
:
... lines 1 - 93 | |
public function testPATCHProgrammer() | |
{ | |
$this->createProgrammer(array( | |
'nickname' => 'CowboyCoder', | |
'avatarNumber' => 5, | |
'tagLine' => 'foo', | |
)); | |
$data = array( | |
'tagLine' => 'bar', | |
); | |
$response = $this->client->patch('/api/programmers/CowboyCoder', [ | |
'body' => json_encode($data) | |
]); | |
$this->assertEquals(200, $response->getStatusCode()); | |
$this->asserter()->assertResponsePropertyEquals($response, 'avatarNumber', 5); | |
$this->asserter()->assertResponsePropertyEquals($response, 'tagLine', 'bar'); | |
} | |
... lines 112 - 124 |
In reality, PATCH can be more complex than this, and we talk about that in our other REST screencast (see The Truth Behind PATCH). But most API's make PATCH work like this.
Make sure the test fails - filter it for PATCH
to run just this one:
phpunit -c app --filter PATCH
Sweet! 405, method not allowed. Time to fix that!
Since PUT and PATCH are so similar, we can handle them in the same action.
Just change the @Method
annotation to have a curly-brace with PUT
and
PATCH
inside of it:
... lines 1 - 87 | |
/** | |
* @Route("/api/programmers/{nickname}") | |
* @Method({"PUT", "PATCH"}) | |
*/ | |
public function updateAction($nickname, Request $request) | |
... lines 93 - 158 |
Now, this route accepts PUT or PATCH. Try the test again:
phpunit -c app --filter PATCH
Woh, 500 error! Integrity constraint: avatarNumber
cannot be null. It is
hitting our endpoint and because we're not sending avatarNumber
, the form
framework is nullifying it, which eventually makes the database yell at us.
The work of passing the data to the form is done in our private processForm()
method. And when it calls $form->submit()
, there's a second argument
called $clearMissing
. It's default value - true
- means that any missing
fields are nullified. But if you set it to false
, those fields are ignored.
That's perfect PATCH behavior. Create a new variable above this line called
$clearMissing
and set it to $request->getMethod() != 'PATCH'
:
... lines 1 - 139 | |
private function processForm(Request $request, FormInterface $form) | |
{ | |
$data = json_decode($request->getContent(), true); | |
$clearMissing = $request->getMethod() != 'PATCH'; | |
... line 145 | |
} | |
... lines 147 - 158 |
In other words, clear all the missing fields, unless the request method is PATCH. Pass this as the second argument:
... lines 1 - 139 | |
private function processForm(Request $request, FormInterface $form) | |
{ | |
$data = json_decode($request->getContent(), true); | |
$clearMissing = $request->getMethod() != 'PATCH'; | |
$form->submit($data, $clearMissing); | |
} | |
... lines 147 - 158 |
Head back, get rid of the big error message and run things again:
phpunit -c app --filter PATCH
Boom! We've got PUT
and PATCH
support with about 2 lines of code.
Hey azeem
The thing is that in Symfony4.3 there is a new feature that enables auto-validation based on your DB constraints set on your Entity (https://symfony.com/blog/ne... ). Either you disable it or allow such field to be null (or pass that value in from your request)
Cheers!
thanks for the reply. Doesn't make sense to throw this validation on PATCH request. Table columns are already set in the database. $form->submit($data, false) should be able to take partial payload for updates while ignoring other columns. I can't find any docs on how to disable it. Tried reverting back to 4.2.*. This is happening even in symfony 4.2.9.
Ohh, so I believe your logic for clearing missing fields is wrong. Double check that you are not clearing missing fields on a patch request
$form->submit($data, $clearMissing);
Using the same logic as in this tutorial<br />$clearMissing = $request->getMethod() !== Request::METHOD_PATCH;<br />$form->submit($data, $clearMissing);<br />
It works if I revert back to Symfony 4.1. But, throws can't be null validations in both symfony 4.2 and 4.3.
Hey azeem!
I was pretty sure that you were right that this was the new auto-validation feature kicking in... until you mentioned that you get the same error in 4.2 (where that feature didn't exist!). So, let's assume that it is NOT that feature. In fact, to debug it, let's use Symfony 4.2. Can you see which field the "this value should not be null" is being attached to in Symfony 4.2? One way to find out is to:
A) Make the request
B) Go to /_profiler
C) Click the little "hash" link on the top request in this list (well, your API request should be the top or second to top)
D) Click the "Forms" tab on the left.
This should allow you to see which field this is attached to.
Here's my guess, and you maybe already know this ;). My guess is that you DO have a @Assert\NotNull
constraint on some field that is already saved to the database as null (maybe because you have some other way of setting this field, and it is ok then for it to be null). Then, when you change some OTHER field on your PATCH request, you're seeing the validation error form this unrelated field. If so, great! This is a totally normal situation. The answer is to use validation groups - here's a good example / use-case for those: https://symfonycasts.com/screencast/symfony3-security/conditional-validation-groups
Let me know if that helps!
Cheers!
thanks weaverryan . I am getting This value should not be blank.
for User.plainPassword
field. Tried this on both symfony 4.2 and 4.3. Adding @Assert\NotBlank(groups={"Create"})
in User entity did not fix it either.
My UserType class has 'validation_groups' => ['Default', 'Create']
And UserController::newAction() has:
`
$form = $this->createForm(UserType::class, $user, [
'validation_groups' => 'Create'
]);
`
azeem Fixed: Had to add validation_groups array in UserController::updateAction() as well. I.e.,
`
$form = $this->createForm(UserType::class, $user, [
'validation_groups' => ['Default']
]);`
Hi guys,
When I run this to create a new entity with missing arguments I am getting an API problem exception with a 400 error code. However when I run a patch request with empty fields I get a 500 error code, any idea why this is?
Hey Shaun T.
Let's double check two things:
* @Method({"PUT", "PATCH"})
What error message do you get?
Cheers!
Hey MolloKhan , thanks for getting back to me, yes I am allowing PUT AND PATCH, and I am doing the $clearMissing thing ;)
Here are the methods for you to see:
// CommentController
/**
* @Route("/api/comments/{id}", name="api_comments_update")
* @Method({"PATCH", "PUT"})
* @param Comment $comment
* @param Request $request
* @return Response
* @Security("is_granted('update', comment)")
*/
public function update(Comment $comment, Request $request)
{
$form = $this->createForm(CommentType::class, $comment);
$this->processForm($request, $form);
if (!($form->isSubmitted() && $form->isValid())) {
$this->throwApiProblemValidationException($form);
}
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
$response = $this->createApiResponse($comment, 200, ['comments_new']);
return $response;
}
// Base Controller
/**
* @param Request $request
* @param FormInterface $form
*/
protected function processForm(Request $request, FormInterface $form)
{
$data = json_decode($request->getContent(), true);
if ($data === null) {
$apiProblem = new ApiProblem(
400,
ApiProblem::TYPE_INVALID_REQUEST_BODY_FORMAT
);
throw new ApiProblemException($apiProblem);
}
$clearMissing = $request->getMethod() != 'PATCH';
$form->submit($data, $clearMissing);
}
And this is the error message I get:
{
"error": {
"code": 500,
"message": "Internal Server Error",
"exception": [
{
"message": "Type error: Argument 1 passed to App\\Entity\\Comment::setComment() must be of the type string, null given, called in /home/vagrant/code/project/vendor/symfony/property-access/PropertyAccessor.php on line 527",
"class": "Symfony\\Component\\Debug\\Exception\\FatalThrowableError",
"trace": [
{
"namespace": "",
"short_class": "",
"class": "",
"type": "",
"function": "",
"file": "/home/vagrant/code/project/src/Entity/Comment.php",
"line": 126,
"args": []
},
{
"namespace": "App\\Entity",
"short_class": "Comment",
"class": "App\\Entity\\Comment",
"type": "->",
"function": "setComment",
"file": "/home/vagrant/code/project/vendor/symfony/property-access/PropertyAccessor.php",
"line": 527,
"args": [
[
"null",
null
]
]
},
Ohh, I see the problem, you are passing a null to Comment::setComment()
method, you have two options, allow nulls to be passed in, or stop passing nulls :)
Cheers!
Thanks Diego,yes I get that part. The issue I have is that when I leave the comment blank on a POST request I get a 400 error code, however with patch I get a 500 error code. Can you explain why PATCH isn’t triggering the api problem exception class?
Because when you are processing a "PATCH" request, it will only persist the field changes coming from the request (because of $clearMissing parameter), all other values will be set to "null", hence it will pass null to Comment::setComment()
Thanks Diego, so how can I resolve this so that it triggers the API Problem exception if an empty comment is submitted?
I have tried removing clearMissing, but I get the same error...
The easiest solution I can think of is to allow passing null values to your setter method. It's not the best option but some times PHP7 strict values can cause some problems
Hi
Great tutorial.
But I'm having a strange behavior with PATCH (more particularly with the clearMissing option ... I think).
So I want to edit an User who have a role "ROLE_ONE" (roles is an array attribute of my User entity) and replace current role with another, call it "ROLE_TWO".
The json I sent looks like
{
....
(some values only for PUT which needs the complete resource)
....
'roles' : [
'ROLE_TWO'
]
}
With PUT, User is correctly updated. getRoles return an array with only one element (ROLE_TWO).
But with PATCH, getRoles return an array with both roles : ['ROLE_ONE', 'ROLE_TWO'].
After debugging a little, I found that it's because in PATCH method, I submit the form with clearMissing at false.
Is it a normal behavior ? If so, how can I replace my array (and not concatenate it) with PATCH.
Or maybe I'm totally wrong and the behavior comes from other place.
Thanks again.
Hey again Chuck!
Well, this is really interesting :). PATCH - at least the way it's most commonly used - means "update the existing data with this new data, but don't remove any existing fields". So, when you have an array property like this, it's not clear *what* the expected behavior should be. I can certainly see why you'd expect the roles field to be "updated" for both PUT and PATCH, meaning in both cases you would have ROLE_TWO only. But, since PATCH means "don't clear any existing data", you can also see why it makes sense to have ROLE_ONE and ROLE_TWO. It's really interesting!
So, I think the behavior is expected, at least in the Symfony world - as seen by how the form component is handling this with clearMissing set to false. But of course, you should do whatever works best for your API. However, I think you would need to add some manual code to make PATCH have this behavior - I can't think of a way to do this with the form (i.e. I can't think of an option you could set on that field in the form to make it behave this way).
Cheers!
Hi Ryan,
Thanks for your reply.
Ok, I understand now why its an expected behavior.
I "hacked" it this way for having a behavior corresponding to what I want : in the preSubmit event, I do the following :
$data = $event->getData();
$form = $event->getForm();
$form->get('roles')->setData($data['roles']);
Do you think its an acceptable way ?
Thanks again.
Hey Chuck!
Yep, this looks fine to me - a solution was either going to be like this, or in your controller. I like it!
Cheers!
// composer.json
{
"require": {
"php": ">=5.3.3",
"symfony/symfony": "2.6.*", // v2.6.11
"doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
"doctrine/dbal": "<2.5", // v2.4.4
"doctrine/doctrine-bundle": "~1.2", // v1.4.0
"twig/extensions": "~1.0", // v1.2.0
"symfony/assetic-bundle": "~2.3", // v2.6.1
"symfony/swiftmailer-bundle": "~2.3", // v2.3.8
"symfony/monolog-bundle": "~2.4", // v2.7.1
"sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
"sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
"incenteev/composer-parameter-handler": "~2.0", // v2.1.0
"hautelook/alice-bundle": "0.2.*", // 0.2
"jms/serializer-bundle": "0.13.*" // 0.13.0
},
"require-dev": {
"sensio/generator-bundle": "~2.3", // v2.5.3
"behat/behat": "~3.0", // v3.0.15
"behat/mink-extension": "~2.0.1", // v2.0.1
"behat/mink-goutte-driver": "~1.1.0", // v1.1.0
"behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
"phpunit/phpunit": "~4.6.0" // 4.6.4
}
}
$form->submit($data, false) fails with "this value should not be null" validation error on PATCH for Symfony 4.3. Might have something to do with https://symfony.com/blog/ne...