Implementing PATCH for Users


In this video we are going to finish off the User feature by implementing the PATCH method. We will cover how the form logic is largely abstracted, making it re-usable by all the subsequent actions (POST, PUT) that we implement in forthcoming controllers for Accounts and Files.

The workflow for PATCH can be broken down in to a few smaller steps. From the UserController's point of view, there are really only three steps:

  • Check the logged in User is accessing their own data
  • (Try to) update the existing User data with the submitted form data
  • Tell the User where they can find their updated data

The controller doesn't really do very much heavy lifting. As we've discussed in previous videos, the controller largely delegates any real responsibility to a -low paid minion- more specialised service.

We're making use of the restricted User repository that we've covered in previous videos to firstly check if the currently logged in User is able to access the requested User's data. As we've already covered, this is handled by a Symfony Voter, and will throw an exception (resulting in a 403 error) if the User doesn't have access to the requested resource.

That's the first step out of the way.

If the controller hasn't errored out by this point, we can assume that the User is trying to update their own data. So far, so good.

PATCHing Like An Idiot

There's a well known article by Will Durand which I referenced in the [previous course on FOSRESTBundle] here on CodeReviewVideos. It's all about PATCHing like an idiot.

Well, my theory on this is - if you are creating a RESTful API for HSBC, or Barclays, or some other financial institution, then by all means make sure you have some military grade checks in place to ensure you are PATCHing atomically.

For the rest of us (no pun intended), a little pragmatism is fine. At least, I haven't been stung yet using this implementation. Fingers crossed, and all that.

From the controller, we start the PATCH process like so:

/** @var $user \AppBundle\Entity\User */
$user = $this->getUserHandler()->patch(
    $requestedUser,
    $request->request->all()
);

The UserHandler is the same one we used in the GET method, which is to say it is returning the 'restricted' UserHandler.

    /**
     * @return UserHandler
     */
    private function getUserHandler()
    {
        return $this->container->get('crv.handler.restricted_user_handler');
    }

If you're not a fan of service location, feel free to create your controllers as a service.

The patch method of the UserHandler is as follows:

    /**
     * @param UserInterface     $user
     * @param array             $parameters
     * @param array             $options
     * @return UserInterface
     */
    public function patch($user, array $parameters, array $options = [])
    {
        if ( ! $user instanceof UserInterface) {
            throw new \InvalidArgumentException('Not a valid User');
        }

        $user = $this->formHandler->handle(
            $user,
            $parameters,
            Request::METHOD_PATCH,
            $options
        );

        $this->repository->save($user);

        return $user;
    }

There are a few interesting points to note here.

Firstly, because we are using a shared interface for all our *Handler services (AccountHandler, FileHandler, etc), we can't type hint the method with our UserInterface. This is unfortunate, and leads to the need for a guard statement (throw if not an instance of `UserInterface).

$parameters is the data submitted by the API user. Another term for this may be 'submitted data', or 'form data'. You're free to use your own wording.

$options becomes useful when your API gets a little more complex. You may wish to only run certain Symfony Validations when in a POST, or others when in a PUT. This is one way of achieving this goal. For now, our $options array will be empty.

If anything untoward happens during the handle method on our formHandler, the form will throw and we will never get to the stage of saving via the repository.

Finally, return the updated $user object if everything went well.

The Fine Art of Delegation

Once again, we delegate the real hard work of handling a form submission to yet another service. In this case, the FormHandler service:

<?php

namespace AppBundle\Form\Handler;

use AppBundle\Exception\InvalidFormException;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormTypeInterface;

class FormHandler implements FormHandlerInterface
{
    /**
     * @var FormFactoryInterface
     */
    private $formFactory;

    /**
     * @var FormTypeInterface
     */
    private $formType;

    /**
     * FormHandler constructor.
     * @param FormFactoryInterface $formFactory
     * @param FormTypeInterface $formType
     */
    public function __construct(
        FormFactoryInterface $formFactory,
        FormTypeInterface $formType
    )
    {
        $this->formFactory = $formFactory;
        $this->formType = $formType;
    }

    /**
     * @param mixed  $object
     * @param array  $parameters
     * @param string $method
     * @param array  $options
     * @return mixed
     * @throws InvalidFormException
     */
    public function handle($object, array $parameters, $method, array $options = [])
    {
        $options = array_replace_recursive([
            'method'            => $method,
            'csrf_protection'   => false,
        ], $options);

        $form = $this->formFactory->create(get_class($this->formType), $object, $options);

        $form->submit($parameters, 'PATCH' !== $method);

        if (!$form->isValid()) {
            throw new InvalidFormException($form);
        }

        return $form->getData();
    }
}

There's some interesting things happening here, and one change between a Symfony 2 implementation of this code, and Symfony 3.

Each different entity type that we implement will lead to a specific service definition inside services.yml. Here is the definition for User:

    crv.form.handler.restricted_user_form_handler:
        class: AppBundle\Form\Handler\FormHandler
        arguments:
            - "@form.factory"
            - "@crv.form.type.restricted_user"

We pass in the Symfony Form Factory service. This allows forms to be instantiated using Symfony's Form Component behind the scenes. That's one part of the heavy lifting delegated very nicely.

Next, we need to pass in our form type. A form type is a funky way of saying... our Symfony form. I never really understood why they used the word 'type' to mean a form... but that's straying off-topic.

In Symfony 2 we could pass in the form type to the form factory and let the form factory create method worry about how to create the right form.

In Symfony 3, that's changed. Instead, we must pass in a string representing our fully namespaced form type class. That explains this horrible line:

$form = $this->formFactory->create(get_class($this->formType), $object, $options);

The other interesting line is :

$form->submit($parameters, 'PATCH' !== $method);

submit takes a boolean as it's second parameter. If that boolean is true, then any missing fields from the form submission will be nulled. This will likely lead to validation errors, or worse, your entities' state suddenly quite wrong. Be very careful of this. Follow the video for a further explanation.

This essentially allows us to keep re-using this same core form logic over and over, whenever we need to submit a form. Less code, less bugs. And also, our Behat test suite will continually test this code many, many times.

Speaking of Behat...

Behat PATCH Tests

By now we have a working PATCH action that:

  • partially updates User objects
  • is secured
  • returns the expected status code

Actually, not so much on that last one. At least, not from the point of view of Behat.

If we test the PATCH functionality in Postman (other REST clients are available ;)) then we should be getting the expected outcome - updated data in the database, 204 status code, and a link to our updated resource.

However, the Behat test is still failing... why?


  Scenario: User can PATCH to update their personal data
    When I send a "PATCH" request to "/users/u1" with body:
      """
      {
        "email": "peter@something-else.net",
        "current_password": "testpass"
      }
      """
    Then the response code should be 204
     And I send a "GET" request to "/users/u1"
     And the response should contain json:
      """
      {
        "id": "u1",
        "email": "peter@something-else.net",
        "username": "peter"
      }
      """

That all looks good. We need to ensure we pass in our current password for security purposes. Even though we are authenticated, we still want our User's to submit their password whenever they are updating their User data. This is an extra safety measure, and it only applies to our User endpoint.

We need to look further up the Behat feature for the answer as to why our test fails:

  Background:
    Given there are Users with the following details:
    | uid | username | email          | password |
    | u1  | peter    | peter@test.com | testpass |
    | u2  | john     | john@test.org  | johnpass |
#    And there are Accounts with the following details:
#    | uid | name     | users |
#    | a1  | account1 | u1    |
    And I am successfully logged in with username: "peter", and password: "testpass"
#    And when consuming the endpoint I use the "headers/content-type" of "application/json"

Yes, way back we commented out the line about setting the Content-type header to application/json. Oops. Behat is sending in text instead.

But shockingly, our API is still reporting a 204 status code. This is not only a bit pants, but is terrible for our developer / consumer user experience.

In the next video, we will fix this. Let me tell you, that was an enjoyable way to spend a few hours one evening whilst trying to debug that little issue ;)

Code For This Course

Get the code for this course.

Episodes