API - PUT and PATCH to Update


We've now seen how to Create (POST) and Retrieve (GET) items from our API, covering two of the letters in CRUD along the way. Next up, we will implement the Update part, which can be done either with PATCH or PUT, depending on your personal preference. As it is, we will implement both.

We can cover both PATCH and PUT in one short video because the implementations are so similar that it's essentially copy / paste, with a one line change.

In fact, we've done the majority of the hard work already - as both PATCH and PUT can be based heavily off the POST / postAction.

Let's quickly review the putAction in full:

    public function putAction(Request $request, int $id)
    {
        /**
         * @var $blogPost BlogPost
         */
        $blogPost = $this->getBlogPostRepository()->find($id);

        if ($blogPost === null) {
            return new View(null, Response::HTTP_NOT_FOUND);
        }

        $form = $this->createForm(BlogPostType::class, $blogPost, [
            'csrf_protection' => false,
        ]);

        $form->submit($request->request->all());

        if (!$form->isValid()) {
            return $form;
        }

        $em = $this->getDoctrine()->getManager();
        $em->flush();

        $routeOptions = [
            'id' => $blogPost->getId(),
            '_format' => $request->get('_format'),
        ];

        return $this->routeRedirectView('get_post', $routeOptions, Response::HTTP_NO_CONTENT);
    }

And now, we will break it down, line by line:

    public function putAction(Request $request, int $id)
    {
        /**
         * @var $blogPost BlogPost
         */
        $blogPost = $this->getBlogPostRepository()->find($id);

Unlike in the postAction, to do an update (PATCH or PUT), we must surely already know which resource / BlogPost we would like to update. Therefore, we must know the id.

Given this, we can figure out that to send in an update, our route is going to be either:

PUT /posts/{$id}

or

PATCH /posts/{id}

With PHP7 we can type hint the $id as an int. What a futuristic world we now live in.

As we know the id, we can do a query for the entity matching that id. This will come in handy in a few places, and is different to the postAction where we can only get the entity once the form has been submitted.

But first:

        if ($blogPost === null) {
            return new View(null, Response::HTTP_NOT_FOUND);
        }

It's a good idea to check if that query returned anything, and if not, best throw the old 404 error. Now, I forgot to do this in the video, my mistake.

Next, we can create the form type for BlogPost entities, and this time - unlike in the postAction - we can pre-populate the form with our existing BlogPost entity:

        $form = $this->createForm(BlogPostType::class, $blogPost, [
            'csrf_protection' => false,
        ]);

If this were going to be a HTML representation, then we would get to see the existing data displaying on the rendered form in our browser. In this case however, we pre-populate the form but the end-user wouldn't really know this had happened. It's not their concern. Later, when we do this in Angular or React we would reload the form and use GET request to get the data to pre-populate, effectively a two step process.

Much like the postAction we can bypass the call to then handleRequest method usually associated with Symfony forms, as we know our form will have been submitted at this stage:

        $form->submit($request->request->all());

I have to say the syntax to get access to the incoming data has never been my favourite - $request->request->all() - simply means to get access to all of the request parameters, on the Request object... which is more confusing sounding that it ought to be.

My advice is use the dump($request); statement immediately before this if you are at all unsure what this might contain.

        if (!$form->isValid()) {
            return $form;
        }

We have no validation constraints so you should be fine. But if you have made a boob somehow then at this stage we would return the entire $form variable contents, which FOSRESTBundle would helpfully intercept and transform into a big JSON representation containing any error messages. It's very handy, if somewhat verbose.

However, if you don't get any errors then:

        $em = $this->getDoctrine()->getManager();
        $em->flush();

As our BlogPost entity is already managed by Doctrine, there is no need to call persist. We only need to call flush and our changes will be saved off to the database. If you are unsure about this, then consider watching this video where this is covered in more depth.

Lastly, we want to return a 204 status code to say things went well, but there is no response to return. After all, the user already knows the content... they just PUT it in to the system!

        $routeOptions = [
            'id' => $blogPost->getId(),
            '_format' => $request->get('_format'),
        ];

        return $this->routeRedirectView('get_post', $routeOptions, Response::HTTP_NO_CONTENT);

The response headers would contain a handy link to the updated resource though, so that's nice.

PATCH'ing Things Up

As mentioned right at the start of this write up, there is only one difference between PATCH and PUT, and it lives on the submit method:

$form->submit($request->request->all(), false);

The default second parameter - clear missing - is true.

By default, if you don't submit a field, Symfony will set the value of the missing field to null.

With a PATCH, we are telling our end users that they don't need to submit every single field, just the ones that have changed.

It would be pretty mean of us to null off any existing data in this case :)

Essentially PATCH is a partial update. I have written more about this here and here.

At this stage we have implemented all but DELETE, the last letter in the CRUD acronym. We will sort that out in the very next video.

Code For This Video

Get the code for this video.

Episodes