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