I’ve often struggled with the PATCH request method in the past. Of course it should enable an API user to only send a subpart of the document to update. But I never found a good solution how to implement a PATCH request the works out in all needed cases.

Last week at the WeAreDevelopers World Congress 2018 in Vienna I listened to a talk about json:api by Jeremiah Lee and he mentioned JSON Patch for incremental updates on JSON documents.

I looked at the specification and found that this easy and small definition of each operation on a JSON document would fulfill my needs on PATCH requests.

In this article I will show you how to implement two operations from the JSON Patch specification for a simple Laravel use case.

The model / JSON document

We will look at a user resource reflected by a user model with the following properties:

  • id
  • name
  • hobbies (an array of strings)
  • email

this data model would be represented with the following JSON document

{
    "id": 1,
    "name": "John Doe",
    "email": "john.doe@example.com",
    "hobbies": [ "Tennis","Golf" ]
}

An operation

The JSON Patch specification says that it is necessary to parse and validate all operations in a PATCH request. If one of the operations is invalid or fails, all operations in this batch should be reverted. For the sake of simplicity we will split each operation into two parts (method calls). At first the validate method must be called to check if the given operation is valid and also valid for a certain eloquent model instance and then the apply method needs to be triggered to actually execute the data manipulation.

For parsing operations we will also introduce another method that reads the raw operations from the request and transforms them into executable portions in our application.

abstract class Operation
{
    public static function parse(array $rawOperations, Model $model) : Collection
    {
        $operations = collect($rawOperations)
            ->map(function ($operation) {
                // TODO: transformation
            });

        $operations
            ->each(function (Operation $op) use ($model) {
                $op->validate($model);
            });

        return $operations;
    }

    /**
     * checks if the operation is valid and can be applied
     * to the given model instance
     *
     * @throws Execption in case of a validation error
     */
    public abstract function validate(Model $model);

    /**
     * applies the operation to a model instance
     */
    public abstract function apply(Model $model);
}

Referencing a JSON attribute

To be able to reference a certain JSON attribute in a PATCH operation it is necessary to introduce another specification called JSON Pointer. A JSON pointer allows to define the full path to a certain JSON value. This is done by seperating the nodes to the value with a “/”.

Let’s look at the JSON document from above again.

{
    "id": 1,
    "name": "John Doe",
    "email": "john.doe@example.com",
    "hobbies": [ "Tennis","Golf" ]
}

If I want to point to the name field the following JSON Pointer is used: /email

To address the hobby “Golf” the pointer /hobbies/1 is used.

Updating an attribute

To update an existing field of the model the JSON Patch specification provides a “replace” operation. This operation is defined by the following JSON object:

{
    "op": "replace",
    "path": "/name",
    "value": "Jane Doe"
}

If this operation is applied it of course updates the name attribute of the user to “Jane Doe”.

To do so we need to implement a ReplaceOperation:

class ReplaceOperation extends Operation
{
    private $path;
    private $value;

    public function __construct(string $path, $value)
    {
        $this->path = $path;
        $this->value = $value;
    }

    public function validate(Model $model)
    {
        if (!array_key_exists($this->getField(), $model->getAttributes())) {
            throw new \Exception('field to replace does not exist');
        }
    }

    public function apply(Model $model)
    {
        $model->{$this->getField()} = $this->value;
    }

    private function getField()
    {
        return substr($this->path, 1);
    }
}

To be able to use the replace operation we need to add it to our parse method in the Operation class.

switch ($operation['op']) {
    case 'replace':
        return new ReplaceOperation($operation['path'], $operation['value']);
        break;
}

Now that we have defined our first operation we can implement the endpoint/route definition.

Route::patch('/user/{user}', function (User $user) {
    $operations = Operation::parse(request()->all(), $user);

    $operations->each->apply($user);
    $user->save();

    return $user;
});

At first we parse the operations in our request, then we use the higher order method each on the operation collection to apply each operation on the user model, then save the user instance and finally return the document.

Adding a hobby

The second operation we will implement is the “add” operation. In our case we would like to add new hobbies to a user document. This can be done with the following implementation of AddOperation.

class AddOperation extends Operation
{
    private $path;
    private $value;

    public function __construct(string $path, $value)
    {
        $this->path = $path;
        $this->value = $value;
    }

    public function validate(Model $model)
    {
        if (!array_key_exists($this->getField(), $model->getAttributes())) {
            throw new \Exception('field to add to does not exist');
        }

        if (!is_array($model->{$this->getField()})) {
            throw new \Exception('field is not of type array');
        }
    }

    public function apply(Model $model)
    {
        $model->{$this->getField()} = array_merge($model->{$this->getField()}, [$this->value]);
    }

    private function getField()
    {
        return substr($this->path, 1);
    }
}

Of course we need to add another case to the switch statement in Operation::parse to use the AddOperation class.

Wrap up

As you can see it is very simple to implement JSON Patch in a Laravel application. The examples showed above are very basic ones and they will need a lot of refactoring and extension to be fully functioning in all cases.

I think that this specification is exactly the solution for PATCH requests and I will definitely take a closer look at all available operations and will use them in my API projects.

Resources