Partially Updating with PATCH [FOSRESTBundle]
Much like in our Symfony 4 JSON API PATCH
implementation, the PATCH
implementation for FOSRESTBundle will be extremely similar to our putAction
.
FOSRESTBundle once again provides us with a convention to follow. We need to name our controller method as patchAction
, and FOSRESTBundle will take care of most of the heavy lifting for us.
As a heads up: this isn't a true PATCH
implementation. It's one pragmatic approach, but it's neither essential to implement nor potentially a good match for your needs. I've written about this before, so please read that link if at all curious.
Anyway, with the 'disclaimer' out of the way, let's first see the test, then cover the implementation.
The PATCH
Test
We have our existing PATCH
test:
Scenario: Can update an existing Album - PATCH
Given the request body is:
"""
{
"track_count": 10
}
"""
When I request "/album/2" using HTTP PATCH
Then the response code is 204
The difference between a PUT
and a PATCH
is that patch will allow updates via partial representations of our entities.
In other words, we only need to submit the fields that change - we don't need to send every field, even if it hasn't changed.
Now, in the real world, I rarely use PATCH
.
From a JavaScript front end it's just much easier to JSON.stringify({...})
a full object, which we would almost always likely have thanks to GET
requests.
But PATCH
might be useful to you, and it's so easy to implement that we're going to add it anyway.
The `PATCH Implementation
We'll start with the basic patchAction
:
public function patchAction(Request $request, string $id)
{
}
And again, this simple bit of code is enough for FOSRESTBundle to configure our PATCH
route:
bin/console debug:router
-------------------------- -------- -------- ------ -----------------------------------
Name Method Scheme Host Path
-------------------------- -------- -------- ------ -----------------------------------
cget_album GET ANY ANY /album
get_album GET ANY ANY /album/{id}
post_album POST ANY ANY /album
put_album PUT ANY ANY /album/{id}
patch_album PATCH ANY ANY /album/{id}
-------------------------- -------- -------- ------ -----------------------------------
I'm going to copy / paste the bulk of this from the putAction
. Honestly, these two are almost identical - why make life harder than it need be?
public function patchAction(Request $request, string $id)
{
$existingAlbum = $this->findAlbumById($id);
$form = $this->createForm(AlbumType::class, $existingAlbum);
$form->submit($request->request->all(), false);
if (false === $form->isValid()) {
return $this->view($form);
}
$this->entityManager->flush();
return $this->view(null, Response::HTTP_NO_CONTENT);
}
The only difference is the second argument to the $form->submit(...)
method.
true
is the default. false
is our explicitly passed argument.
PHP makes this a little hard to understand. The second argument is whether to clearMissing
. The latest versions of PhpStorm actually makes this much more evident:
But what is clearMissing
?
Well, let's take a look at the interface:
// vendor/symfony/form/FormInterface.php
/**
* Submits data to the form, transforms and validates it.
*
* @param mixed $submittedData The submitted data
* @param bool $clearMissing Whether to set fields to NULL when they
* are missing in the submitted data
*
* @return $this
*
* @throws Exception\AlreadySubmittedException if the form has already been submitted
*/
public function submit($submittedData, $clearMissing = true);
So clearMissing
is going to null
any fields we do not provide values for.
In a PATCH
situation this is exactly not at all what we want.
We want to keep the existing values, and only update the properties that we explicitly provide. By setting clearMissing
to false
, this is exactly what we get.
There's not much more to it than this.
vendor/bin/behat features/album_symfony_4.feature --tags=t
Feature: Provide insight into how Symfony 4 behaves on the unhappy path
In order to eliminate bad Album data
As a JSON API developer
I need to ensure Album data meets expected criteria
Background: # features/album_symfony_4.feature:7
Given there are Albums with the following details: # FeatureContext::thereAreAlbumsWithTheFollowingDetails()
| title | track_count | release_date |
| some fake album name | 12 | 2020-01-08T00:00:00+00:00 |
| another great album | 9 | 2019-01-07T23:22:21+00:00 |
| now that's what I call Album vol 2 | 23 | 2018-02-06T11:10:09+00:00 |
@symfony_4_edge_case @t
Scenario: Can update an existing Album - PATCH # features/album_symfony_4.feature:56
Given the request body is: # Imbo\BehatApiExtension\Context\ApiContext::setRequestBody()
"""
{
"track_count": 10
}
"""
When I request "/album/2" using HTTP PATCH # Imbo\BehatApiExtension\Context\ApiContext::requestPath()
Then the response code is 204 # Imbo\BehatApiExtension\Context\ApiContext::assertResponseCodeIs()
1 scenario (1 passed)
4 steps (4 passed)
0m0.16s (9.74Mb)
As we travel down the happy path, all we have left now is DELETE
.