Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

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.

Start your All-Access Pass
Buy just this tutorial for $10.00

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!

Leave a comment!

7
Login or Register to join the conversation
Default user avatar
Default user avatar Nina | posted 5 years ago | edited

Hello
I want to validate data and write in the controller:


  if ($form->isSubmitted() && $form->isValid()) {}

but $form->isValid() don't work

UserController.php


/**
     * @Route("/users/{id}/edit", name="edit_user")
     * @Method("PUT")
     */
    public function editUser($id, Request $request, User $user)
    {

        $em = $this->getDoctrine()->getManager();
        $user = $em->getRepository('AppBundle:User')
            ->findOneBy(['id' => $id]);

        $data = json_decode($request->getContent(), true);

        $form = $this->createForm(AddUserForm::class, $user);
        $form->submit($data);
      
        if ($form->isSubmitted() && $form->isValid()) {
            $user = $form->getData();
           $em->persist($user);
            $em->flush();
        }
         
        return new Response('User updated');

how could I validate data in this case?

Reply

Hi Nina,

What is your question? I don't see any text except "Hello" ;)

Cheers!

Reply
Default user avatar

And now )

Reply

Hey Nina,

Do you get any errors when trying to validate ?
Also you are injecting an User object to your controller action, and you never use it, you should only rely on the ID

BTW, next course is all about errors, you may want to hold a little bit before adding validations :)

Reply
Default user avatar

Yes, thanks I see the next course.
And now I use :
$validator = $this->get('validator');
$errors = $validator->validate($user);
if(!$errors){
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
}
$data = $this->container->get('jms_serializer')->serialize($errors, 'json');
return new Response($data);

and I got JSON response with error
this what I want to give

Thanks again for your lessons and help

Reply

Excellent! Feel free to contact us if you have more questions about our tutorials :)

Reply
Default user avatar
Default user avatar samir Patel | posted 5 years ago

Hi. Not sure if this is happening to anyone else but there is no audio on this video? Thanks!
*Edit: Sorry it's my computer, ignore.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony. The concepts of REST are still valid, but I recommend using API Platform in new Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.*", // v2.6.11
        "doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
        "doctrine/dbal": "<2.5", // v2.4.4
        "doctrine/doctrine-bundle": "~1.2", // v1.4.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.6.1
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.8
        "symfony/monolog-bundle": "~2.4", // v2.7.1
        "sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
        "sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "0.2.*", // 0.2
        "jms/serializer-bundle": "0.13.*" // 0.13.0
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3", // v2.5.3
        "behat/behat": "~3.0", // v3.0.15
        "behat/mink-extension": "~2.0.1", // v2.0.1
        "behat/mink-goutte-driver": "~1.1.0", // v1.1.0
        "behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~4.6.0" // 4.6.4
    }
}
userVoice