Buy

Weird Endpoint: The tagline as a Resource?

Most of our endpoints are pretty straightforward: We create a programmer, we update a programmer, we create a battle, we get a collection of battles.

Reality check! In the wild: endpoints get weird. Learning how to handle these was one of the most frustrating parts of REST for me. So let's code through two examples.

Updating just the Tagline?

Here's the first: suppose you decide that it would be really nice to have an endpoint where your client can edit the tagline of a programmer directly.

Now, technically, that's already possible: send a PATCH request to the programmer endpoint and only send the tagline.

But remember: we're building the API for our API clients, and if they want an endpoint specifically for updating a tagline, give it to them.

Open ProgrammerControllerTest: let's design the endpoint first. Make a public function testEditTagline():

334 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 317
public function testEditTagline()
{
... lines 320 - 331
}
}

Scroll to the top and copy the $this->createProgrammer() line that we've been using. Give this a specific tag line: The original UnitTester:

334 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 317
public function testEditTagline()
{
$this->createProgrammer(array(
'nickname' => 'UnitTester',
'avatarNumber' => 3,
'tagLine' => 'The original UnitTester'
));
... lines 325 - 331
}
}

The URL Structure

Now, if we want an endpoint where the only thing you can do is edit the tagLine, how should that look?

One way to think about this is that the tagLine is a subordinate string resource of the programmer. Remember also that every URI is supposed to represent a different resource. If you put those 2 ideas together, a great URI becomes obvious: /api/programmers/UnitTester/tagline. In fact, if you think of this as its own resource, then all of a sudden, you could imagine creating a GET endpoint to fetch only the tagline or a PUT endpoint to update just the tagline. It's a cool idea!

And that's what we'll do: make an update request with $this->client->put() to this URL: /api/programmers/UnitTester/tagline:

334 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 317
public function testEditTagline()
{
$this->createProgrammer(array(
'nickname' => 'UnitTester',
'avatarNumber' => 3,
'tagLine' => 'The original UnitTester'
));
$response = $this->client->put('/api/programmers/UnitTester/tagline', [
... lines 327 - 328
]);
... lines 330 - 331
}
}

How to send the Data?

Send the normal Authorization header:

334 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 317
public function testEditTagline()
{
... lines 320 - 325
$response = $this->client->put('/api/programmers/UnitTester/tagline', [
'headers' => $this->getAuthorizedHeaders('weaverryan'),
... line 328
]);
... lines 330 - 331
}
}

But how should we pass the new tagline data? Normally, we send a json-encoded array of fields. But this resource isn't a collection of fields: it's just one string. There's nothing wrong with sending some JSON data up like before, but you could also set the body to the plain-text New Tag Line itself:

334 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 317
public function testEditTagline()
{
... lines 320 - 325
$response = $this->client->put('/api/programmers/UnitTester/tagline', [
'headers' => $this->getAuthorizedHeaders('weaverryan'),
'body' => 'New Tag Line'
]);
... lines 330 - 331
}
}

And I think this is pretty cool.

Finish this off with $this->assertEquals() 200 for the status code:

334 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 317
public function testEditTagline()
{
... lines 320 - 325
$response = $this->client->put('/api/programmers/UnitTester/tagline', [
'headers' => $this->getAuthorizedHeaders('weaverryan'),
'body' => 'New Tag Line'
]);
$this->assertEquals(200, $response->getStatusCode());
... line 331
}
}

But what should be returned? Well, whenever we edit or create a resource, we return the resource that we just edited or created. In this context, the tagline is its own resource... even though it's just a string. So instead of expecting JSON, let's look for the literal text: $this->assertEquals() that New Tag Line is equal to the string representation of $response->getBody():

334 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 329
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('New Tag Line', (string) $response->getBody());
... lines 332 - 334

But you don't need to do it this way: you might say:

Look, we all know that you're really editing the UnitTester programmer resource, so I'm going to return that.

And that's fine! This is an interesting option for how to think about things. Just as long as you don't spend your days dreaming philosophically about your API, you'll be fine. Make a decision and feel good about it. In fact, that's good life advice.

Adding the String Resource Endpoint

Let's finish this endpoint. At the bottom of ProgrammerController, add a new public function editTaglineAction():

184 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 173
public function editTagLineAction(Programmer $programmer, Request $request)
{
... lines 176 - 181
}
}

We already know that the route should be /api/programmers/{nickname}/tagline. To be super hip, add an @Method annotation: we know this should only match PUT requests:

184 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 169
/**
* @Route("/api/programmers/{nickname}/tagline")
* @Method("PUT")
*/
public function editTagLineAction(Programmer $programmer, Request $request)
{
... lines 176 - 181
}
}

Like before, type-hint the Programmer argument so that Doctrine will query for it for us, using the nickname value. And, we'll also need the Request argument:

184 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 7
use AppBundle\Entity\Programmer;
... lines 9 - 16
use Symfony\Component\HttpFoundation\Request;
... lines 18 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 173
public function editTagLineAction(Programmer $programmer, Request $request)
{
... lines 176 - 181
}
}

I could use a form like before... but this is just so simple: $programmer->setTagLine($request->getContent()):

184 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 173
public function editTagLineAction(Programmer $programmer, Request $request)
{
$programmer->setTagLine($request->getContent());
... lines 177 - 181
}
}

Literally: read the text from the request body and set that on the programmer.

Now, save: $em = $this->getDoctrine()->getManager(), $em->persist($programmer) and $em->flush():

184 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 175
$programmer->setTagLine($request->getContent());
$em = $this->getDoctrine()->getManager();
$em->persist($programmer);
$em->flush();
... lines 180 - 184

For the return, it's not JSON! Return a plain new Response() with $programmer->getTagLine(), a 200 status code, and a Content-Type header of text/plain:

184 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 17
use Symfony\Component\HttpFoundation\Response;
... lines 19 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 173
public function editTagLineAction(Programmer $programmer, Request $request)
{
... lines 176 - 180
return new Response($programmer->getTagLine());
}
}

Now, this is a good-looking, super-weird endpoint. Copy the test method name and try it out:

./vendor/bin/phpunit --filter testEditTagLine

We're green! Next, let's look at a weirder endpoint.

Leave a comment!

  • 2016-06-23 Vlad

    Thank you, Victor. I will make a not of that. Useful trick to have.

  • 2016-06-20 Victor Bocharsky

    Hey, Vlad!

    Actually, I don't create my own converters, for something complex I manually query an entity from database. It's more quickly than create a custom converter and is more obvious for complex code.

    The simple mapping controlling looks like:


    /**
    * @Route("/blog/{post_id}")
    * @ParamConverter("post", class="SensioBlogBundle:Post", options={"id" = "post_id"})
    */
    public function showAction(Post $post)
    {
    }


    You should explicitly use ParamConverter annotation. In this example we have an ID property for Post entity, but placeholder's name is post_id, so we need to use a custom mapping here (options={"id" = "post_id"}).

    Search for 'mapping' on Doctrine Converter page to find more mapping examples.

    Cheers!

  • 2016-06-20 Vlad

    Thank you for the explanation, Victor!
    How do you control mapping of route placeholders to the entity properties and even create your own converter?

  • 2016-06-17 Victor Bocharsky

    Hey, Vlad!

    I can help you with it. This feature called ParamConverter in Symfony. Actually, in this case triggered the Doctrine Converter. This guy do all that magic:
    1. Determine entity repository from typehinted entity class in method (Programmer in our case);
    2. Then if route has '{id}' placeholder - ParamConverter calls find() method on its repository, otherwise it try to find entity by criteria using findOneBy()('{nickname}' in our case)
    3. And inject found entity into the method or throw an NotFoundHttpException exception

    So you don't need to have findOneByNickname() method in your entity repository class.

    BTW, you can control mapping of route placeholders to the entity properties and even create your own converter.

    Cheers!

  • 2016-06-16 Vlad

    Hi Ryan,

    How does Doctrine know to query for the Programmer using the `nickname` value? Does it check that the nickname property is part of the Programmer class and then does findOneBy(array('nickname' => $nickname)), or does it need an existing method (findOneByNickname($nickname)) in ProgrammerRepository?

    Thank you!