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

Coding the Subordinate Resource Endpoint

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 $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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

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:

... 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

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:

... 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!

0
Login or Register to join the conversation
Cat in space

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

This tutorial uses an older version of Symfony. The concepts of Hypermedia & HATEOAS are still valid. But I recommend using API Platform in modern Symfony apps.

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice