Course: Symfony RESTful API: Hypermedia, Links & Bonuses (Course 5) Tutorial
Before we code up the endpoint, start with the test. But wait! This test is going to be pretty cool: we'll make a request for a programmer resource and follow that link to its battles.
In ProgrammerControllerTest
, add a new public function testFollowProgrammerBattlesLink()
:
... lines 1 - 7 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 10 - 64 | |
public function testFollowProgrammerBattlesLink() | |
{ | |
... lines 67 - 87 | |
} | |
... lines 89 - 315 | |
} |
Copy the first 2 parts from testGETProgrammer()
that create the programmer and make
the request. Add those here:
... lines 1 - 7 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 10 - 64 | |
public function testFollowProgrammerBattlesLink() | |
{ | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
... lines 71 - 78 | |
$response = $this->client->get('/api/programmers/UnitTester', [ | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
... lines 82 - 87 | |
} | |
... lines 89 - 315 | |
} |
Okay: before the request, we need to add some battles to the database so we have
something results to check out. Create a project first with $this->createProject('cool_project')
:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
... lines 72 - 317 |
Now, let's add 3 battles. And remember! To do that, we need the BattleManager
service. Set that up with $battleManager = $this->getService()
- that's a helper
method in ApiTestCase
- and look up battle.battle_manager
:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
/** @var BattleManager $battleManager */ | |
$battleManager = $this->getService('battle.battle_manager'); | |
... lines 75 - 317 |
Let's add some inline PHPDoc so PhpStorm auto-completes the next lines.
Love it!
Now, life is easy. Add, $battleManager->battle()
and pass it $programmer
:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
/** @var BattleManager $battleManager */ | |
$battleManager = $this->getService('battle.battle_manager'); | |
$battleManager->battle($programmer, $project); | |
... lines 76 - 317 |
And, whoops - make sure you have a $programmer
variable set above. Now, add $project
.
Copy that and paste it 2 more times:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
/** @var BattleManager $battleManager */ | |
$battleManager = $this->getService('battle.battle_manager'); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
... lines 78 - 317 |
And we are setup! After we make the request for the programmer, we should get back
a link we can follow. Get that link with $uri = $this->asserter()->readResponseProperty()
.
Read _links.battles
:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
/** @var BattleManager $battleManager */ | |
$battleManager = $this->getService('battle.battle_manager'); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$response = $this->client->get('/api/programmers/UnitTester', [ | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
$url = $this->asserter() | |
->readResponseProperty($response, '_links.battles'); | |
... lines 84 - 317 |
Make sure you pass $response
as the first argument.
Now, follow that link! Be lazy and copy the $response =
code from above,
because we still need that Authorization
header. But change the url to be our
dynamic $uri
:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
/** @var BattleManager $battleManager */ | |
$battleManager = $this->getService('battle.battle_manager'); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$response = $this->client->get('/api/programmers/UnitTester', [ | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
$url = $this->asserter() | |
->readResponseProperty($response, '_links.battles'); | |
$response = $this->client->get($url, [ | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
... lines 87 - 317 |
Before we assert anything, let's dump the response and decide later how this should all exactly look:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
/** @var BattleManager $battleManager */ | |
$battleManager = $this->getService('battle.battle_manager'); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$response = $this->client->get('/api/programmers/UnitTester', [ | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
$url = $this->asserter() | |
->readResponseProperty($response, '_links.battles'); | |
$response = $this->client->get($url, [ | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
$this->debugResponse($response); | |
... lines 88 - 317 |
Test, check! Let's hook this up. Open ProgrammerController
. At first, it's pretty
easy. Exchange the nickname
for a Programmer
object. I'll use a magic param
converter for this: just type-hint the argument with Programmer
, and it will
magically make the query for us:
... lines 1 - 22 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 25 - 150 | |
/** | |
* @Route("/api/programmers/{nickname}/battles", name="api_programmers_battles_list") | |
*/ | |
public function battlesListAction(Programmer $programmer) | |
{ | |
... lines 156 - 159 | |
} | |
} |
Next, get battles the way you always do: $this->getDoctrine()->getRepository('AppBundle:Battle')
:
... lines 1 - 22 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 25 - 153 | |
public function battlesListAction(Programmer $programmer) | |
{ | |
$battles = $this->getDoctrine()->getRepository('AppBundle:Battle') | |
... lines 157 - 159 | |
} | |
} |
Use findBy()
to return an array that match programmer => $programmer
:
... lines 1 - 22 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 25 - 153 | |
public function battlesListAction(Programmer $programmer) | |
{ | |
$battles = $this->getDoctrine()->getRepository('AppBundle:Battle') | |
->findBy(['programmer' => $programmer]); | |
... lines 158 - 159 | |
} | |
} |
What now? Why not a simple return? return $this->createApiResponse()
and pass it $battles
:
... lines 1 - 22 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 25 - 153 | |
public function battlesListAction(Programmer $programmer) | |
{ | |
$battles = $this->getDoctrine()->getRepository('AppBundle:Battle') | |
->findBy(['programmer' => $programmer]); | |
return $this->createApiResponse($battles); | |
} | |
} |
Right? Is it really that simple?
Well, let's find out! Go back to ProgrammerControllerTest
, copy the new method
name and run:
./vendor/bin/phpunit --filter testFollowProgrammerBattlesLink
OK, cool - check out how this looks: it's a big JSON array that holds a bunch of JSON battle objects. At first glance, it's great! But there's a problem? It's totally inconsistent with our other endpoint that returns a collection of programmers.
Scroll down a little to testProgrammersCollection()
. Here: we expect an items
key
with the resources inside of it:
... lines 1 - 7 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 10 - 105 | |
public function testGETProgrammersCollection() | |
{ | |
... lines 108 - 120 | |
$this->asserter()->assertResponsePropertyIsArray($response, 'items'); | |
$this->asserter()->assertResponsePropertyCount($response, 'items', 2); | |
$this->asserter()->assertResponsePropertyEquals($response, 'items[1].nickname', 'CowboyCoder'); | |
} | |
... lines 125 - 315 | |
} |
We're also missing the pagination fields, making it harder for our API clients to guess how our responses will look.
Nope, we can do better, guys.