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.