Buy

Coding the Subordinate Resource Endpoint

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():

317 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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:

317 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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'):

317 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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:

317 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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:

317 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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:

317 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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:

317 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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:

317 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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:

317 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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

Coding the Subordinate Collection

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:

162 lines src/AppBundle/Controller/Api/ProgrammerController.php
... 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'):

162 lines src/AppBundle/Controller/Api/ProgrammerController.php
... 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:

162 lines src/AppBundle/Controller/Api/ProgrammerController.php
... 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:

162 lines src/AppBundle/Controller/Api/ProgrammerController.php
... 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

Consistency Anyone?

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:

317 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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.

Leave a comment!