Buy Access to Course
14.

PUT is for Updating

Share this awesome video!

|

Keep on Learning!

Suppose now that someone using our API needs to edit a programmer: maybe they want to change its avatar. What HTTP method should we use? And what should the endpoint return? Answering those questions is one of the reasons we always start by writing a test - it's like the design phase of a feature.

Create a public function testPUTProgrammer() method:

// ... lines 1 - 71
public function testPUTProgrammer()
{
// ... lines 74 - 89
}
// ... lines 91 - 92

Usually, if you want to edit a resource, you'll use the PUT HTTP method. And so far, we've seen POST for creating and PUT for updating. But it's more complicated than that, and involves PUT being idempotent. We have a full 5 minute video on this in our original REST screencast (see PUT versus POST), and if you don't know the difference between PUT and POST, you should geek out on this.

Inside the test, copy the createProgrammer() for CowboyCoder from earlier. Yep, this programmer definitely needs his avatar changed. Next copy the request and assert stuff from testGETProgrammer() and add that. Ok, what needs to be updated. Change the request from get() to put(). And like earlier, we need to send a JSON string body in the request. Grab one of the $data arrays from earlier, add it here, then json_encode() it for the body. This is a combination of stuff we've already done:

// ... lines 1 - 71
public function testPUTProgrammer()
{
$this->createProgrammer(array(
'nickname' => 'CowboyCoder',
'avatarNumber' => 5,
'tagLine' => 'foo',
));
$data = array(
// ... lines 81 - 83
);
$response = $this->client->put('/api/programmers/CowboyCoder', [
'body' => json_encode($data)
]);
// ... lines 88 - 89
}
// ... lines 91 - 92

For a PUT request, you're supposed to send the entire resource in the body, even if you only want to update one field. So we need to send nickname, avatarNumber and tagLine. Update the $data array so the nickname matches CowboyCoder, but change the avatarNumber to 2. We won't update the tagLine, so send foo and add that to createProgrammer() to make sure this is CowboyCoder's starting tagLine:

// ... lines 1 - 71
public function testPUTProgrammer()
{
$this->createProgrammer(array(
// ... lines 75 - 76
'tagLine' => 'foo',
));
$data = array(
'nickname' => 'CowboyCoder',
'avatarNumber' => 2,
'tagLine' => 'foo',
);
$response = $this->client->put('/api/programmers/CowboyCoder', [
'body' => json_encode($data)
]);
// ... lines 88 - 89
}
// ... lines 91 - 92

This will create the Programmer in the database then send a PUT request where only the avatarNumber is different. Asserting a 200 status code is perfect, and like most endpoints, we'll return the JSON programmer. But, we're already testing the JSON pretty well in other spots. So here, just do a sanity check: assert that the avatarNumber has in fact changed to 2:

// ... lines 1 - 71
public function testPUTProgrammer()
{
// ... lines 74 - 84
$response = $this->client->put('/api/programmers/CowboyCoder', [
'body' => json_encode($data)
]);
// ... line 88
$this->asserter()->assertResponsePropertyEquals($response, 'avatarNumber', 2);
}
// ... lines 91 - 92

Ready? Try it out, with a --filter testPUTProgrammer to only run this one:

phpunit -c app --filter testPUTProgrammer

Hey, a 405 error! Method not allowed. That makes perfect sense: we haven't added this endpoint yet. Test check! Let's code!

Adding the PUT Controller

Add a public function updateAction(). The start of this will look a lot like showAction(), so copy its Route stuff, but change the method to PUT, and change the name so it's unique. For arguments, add $nickname and also $request, because we'll need that in a second:

// ... lines 1 - 87
/**
* @Route("/api/programmers/{nickname}")
* @Method("PUT")
*/
public function updateAction($nickname, Request $request)
{
// ... lines 94 - 116
}
// ... lines 118 - 130

Ok, we have two easy jobs: query for the Programmer then update it from the JSON. Steal the query logic from showAction():

// ... lines 1 - 91
public function updateAction($nickname, Request $request)
{
$programmer = $this->getDoctrine()
->getRepository('AppBundle:Programmer')
->findOneByNickname($nickname);
if (!$programmer) {
throw $this->createNotFoundException(sprintf(
'No programmer found with nickname "%s"',
$nickname
));
}
// ... lines 104 - 116
}
// ... lines 118 - 130

The updating part is something we did in the original POST endpoint. Steal everything from newAction(), though we don't need all of it. Yes yes, we will have some code duplication for a bit. Just trust me - we'll reorganize things over time. Get rid of the new Programmer() line - we're querying for one. And take out the setUser() code too: that's just needed on create. And because we're not creating a resource, we don't need the Location header and the status code should be 200, not 201:

// ... lines 1 - 91
public function updateAction($nickname, Request $request)
{
// ... lines 94 - 104
$data = json_decode($request->getContent(), true);
$form = $this->createForm(new ProgrammerType(), $programmer);
$form->submit($data);
$em = $this->getDoctrine()->getManager();
$em->persist($programmer);
$em->flush();
$data = $this->serializeProgrammer($programmer);
$response = new JsonResponse($data, 200);
return $response;
}
// ... lines 118 - 130

Done! And if you look at the function, it's really simple. Most of the duplication is for pretty mundane code, like creating a form and saving the Programmer. Creating endpoints is already really easy.

Before I congratulate us any more, let's give this a try:

phpunit -c app --filter testPUTProgrammer

Uh oh! 404! But check out that really clear error message from the response:

No programmer found for username UnitTester

Well yea! Because we should be editing CowboyCoder. In ProgrammerControllerTest, I made a copy-pasta error! Update the PUT URL to be /api/programmers/CowboyCoder, not UnitTester:

// ... lines 1 - 71
public function testPUTProgrammer()
{
// ... lines 74 - 84
$response = $this->client->put('/api/programmers/CowboyCoder', [
'body' => json_encode($data)
]);
// ... lines 88 - 89
}
// ... lines 91 - 92

Now we're ready again:

phpunit -c app --filter testPUTProgrammer

We're passing!

Centralizing Form Data Processing

Before we go on we need to clean up some of this duplication. It's small, but each write endpoint is processing the request body in the same way: by fetching the content from the request, calling json_decode() on that, then passing it to $form->submit().

Create a new private function called processForm(). This will have two arguments - $request and the form object, which is a FormInterface instance, not that that's too important:

// ... lines 1 - 116
private function processForm(Request $request, FormInterface $form)
{
// ... lines 119 - 120
}
// ... lines 122 - 133

We'll move two things here: the two lines that read and decode the request body and the $form->submit() line:

// ... lines 1 - 116
private function processForm(Request $request, FormInterface $form)
{
$data = json_decode($request->getContent(), true);
$form->submit($data);
}
// ... lines 122 - 133

If this looks small to you, it is! But centralizing the json_decode() means we'll be able to handle invalid JSON in one spot, really easily in the next episode.

In updateAction(), call $this->processForm() passing it the $request and the $form. Celebrate by removing the json_decode lines. Do the same thing up in newAction:

// ... lines 1 - 20
public function newAction(Request $request)
{
$programmer = new Programmer();
$form = $this->createForm(new ProgrammerType(), $programmer);
$this->processForm($request, $form);
// ... lines 26 - 41
}
// ... lines 43 - 90
public function updateAction($nickname, Request $request)
{
// ... lines 93 - 103
$form = $this->createForm(new ProgrammerType(), $programmer);
$this->processForm($request, $form);
// ... lines 106 - 114
}
// ... lines 116 - 133

Yay! We're just a little cleaner. To really congratulate ourselves, try the whole test suite:

phpunit -c app

Wow!