Buy

PATCH is (also) for Updating (basically)

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 resoure 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():

125 lines src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
... 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():

124 lines src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
... 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!

Support PUT and PATCH

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:

158 lines src/AppBundle/Controller/Api/ProgrammerController.php
... 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':

158 lines src/AppBundle/Controller/Api/ProgrammerController.php
... 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:

158 lines src/AppBundle/Controller/Api/ProgrammerController.php
... 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.

Leave a comment!

  • 2016-08-06 weaverryan

    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!

  • 2016-08-06 Chuck Norris

    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.

  • 2016-08-05 weaverryan

    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!

  • 2016-08-05 Chuck Norris

    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.