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:

phpstorm-fosrestbundle-clear-missing

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.

Episodes

# Title Duration
1 What will our JSON API actually do? 08:46
2 What needs to be in our Database for our Tests to work? 12:32
3 Cleaning up after each Test Run 02:40
4 Docker makes for Easy Databases 09:01
5 Healthcheck [Raw Symfony 4] 07:53
6 Send in JSON data using POST [Raw Symfony 4] 05:33
7 Keep your data nice and tidy using Symfony's Form [Raw Symfony 4] 10:48
8 Validating incoming JSON [Raw Symfony 4] 08:26
9 Nicer error messages [Raw Symfony 4] 06:23
10 GET'ting data from our Symfony 4 API [Raw Symfony 4] 08:11
11 GET'ting a collection of Albums [Raw Symfony 4] 01:50
12 Update existing Albums with PUT [Raw Symfony 4] 05:00
13 Upsetting Purists with PATCH [Raw Symfony 4] 02:39
14 Hitting DELETE [Raw Symfony 4] 02:11
15 How to open your API to the outside world with CORS [Raw Symfony 4] 07:48
16 Getting Setup with Symfony 4 and FOSRESTBundle [FOSRESTBundle] 09:11
17 Healthcheck [FOSRESTBundle] 06:14
18 Handling POST requests [FOSRESTBundle] 08:31
19 Saving POST data to the database [FOSRESTBundle] 09:44
20 Work with XML, or JSON, or Both [FOSRESTBundle] 04:31
21 Going far, then Too Far with the ViewResponseListener [FOSRESTBundle] 03:19
22 GET'ting data from your Symfony 4 API [FOSRESTBundle] 05:58
23 GET'ting a Collection of data from your Symfony 4 API [FOSRESTBundle] 01:27
24 Updating with PUT [FOSRESTBundle] 02:58
25 Partially Updating with PATCH [FOSRESTBundle] 02:15
26 DELETE'ing Albums [FOSRESTBundle] 01:27
27 Handling Errors [FOSRESTBundle] 08:58
28 Introducing the API Platform [API Platform] 08:19
29 The Entry Point [API Platform] 04:30
30 The Context [API Platform] 05:52
31 Healthcheck - Custom Endpoint [API Platform] 05:17
32 Starting with POST [API Platform] 07:08
33 Creating Entities with the Schema Generator [API Platform] 07:38
34 Defining A Custom POST Route [API Platform] 07:31
35 Finishing POST [API Platform] 06:29
36 GET'ting One Resource [API Platform] 02:50
37 GET'ting Multiple Resources [API Platform] 02:59
38 PUT to Update Existing Data [API Platform] 02:19
39 DELETE to Remove Data [API Platform] 01:15
40 No One Likes Errors [API Platform] 03:28