If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeMost 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.
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()
:
... 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
:
... 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 | |
} | |
} |
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
:
... 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 | |
} | |
} |
Send the normal Authorization
header:
... 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:
... 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:
... 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()
:
... 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.
Let's finish this endpoint. At the bottom of ProgrammerController
, add a new public function editTaglineAction()
:
... 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:
... 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:
... 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())
:
... 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()
:
... 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
:
... 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.
Hey, Vlad!
I can help you with it. This feature called <a href="http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html">ParamConverter</a> in Symfony. Actually, in this case triggered the <a href="http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html#doctrine-converter">Doctrine Converter</a>. This guy do all that magic:
Programmer
in our case);find()
method on its repository, otherwise it try to find entity by criteria using findOneBy()
('{nickname}' in our case)NotFoundHttpException
exceptionSo 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!
Thank you for the explanation, Victor!
How do you control mapping of route placeholders to the entity properties and even create your own converter?
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 <a href="http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html#doctrine-converter">Doctrine Converter</a> page to find more mapping examples.
Cheers!
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.0.*", // v3.0.3
"doctrine/orm": "^2.5", // v2.5.4
"doctrine/doctrine-bundle": "^1.6", // 1.6.2
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // v2.10.0
"sensio/distribution-bundle": "^5.0", // v5.0.4
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
"incenteev/composer-parameter-handler": "~2.0", // v2.1.2
"jms/serializer-bundle": "^1.1.0", // 1.1.0
"white-october/pagerfanta-bundle": "^1.0", // v1.0.5
"lexik/jwt-authentication-bundle": "^1.4", // v1.4.3
"willdurand/hateoas-bundle": "^1.1" // 1.1.1
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.6
"symfony/phpunit-bridge": "^3.0", // v3.0.3
"behat/behat": "~3.1@dev", // dev-master
"behat/mink-extension": "~2.2.0", // v2.2
"behat/mink-goutte-driver": "~1.2.0", // v1.2.1
"behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
"phpunit/phpunit": "~4.6.0", // 4.6.10
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}
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!