PUT is for Updating
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
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!
Hello
I want to validate data and write in the controller:
but $form->isValid() don't work
UserController.php
how could I validate data in this case?