Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

Link to a Subordinate Resource!

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

I'm playing with my API and looking at the collection of programmers. And of course, I can follow the self link to GET just that one resource. But now that I'm here, it occurs to me that it would be really cool if I had a battles link we could follow that would return a collection resource of all of the battles that this programmer has been in. So let's do that.

I'm going to add a new scenario inside programmer.feature, since it'll be showing me all the battles for a programmer. I'll call the scenario: "GET a collection of battles of the programmer". At the start of this scenario, we'll need a few projects, one programmer and a few battles between them. We used some similar language in battle.feature. I'll make sure there's a projectA in the database first and then repeat that to make a projectB. And let's make sure that our favorite programmer Fred exists as well. Finally, we'll add two more lines to create 2 battles between Fred and each project:

... lines 1 - 93
Scenario: GET a collection of battles for a programmer
Given there is a project called "projectA"
Given there is a project called "projectB"
And there is a programmer called "Fred"
And there has been a battle between "Fred" and "projectA"
And there has been a battle between "Fred" and "projectB"
... lines 100 - 144

Cool - so that's all the setup work.

This will return a collection resource, so we can steal a lot of the scenario from above, since all collection resources pretty much look the same.

Simple Guide to URL Structures

For the URI, one of the things you'll hear is that URIs don't matter. In theory, you can make whatever URIs you want. So if you're stressing out about how a URI should look, just choose something, because it ultimately doesn't matter.

That being said, you typically follow a pattern. So far we've seen URLs like /api/programmers for a collection and /api/programmers/Fred for a single programmer. And that's a decent pattern. In this case, this is actually what we call a "subordinate" resource - it's the collection of battles under a specific programmer. So a good URL for this is the URL to a specific programmer, plus /battles to get the subordinate battles collection resource for Fred. After that, everything will be pretty much the same, changing programmers to battles. We'll even check that the first battle has a didProgrammerWin property, since every battle has that. We don't know what it's going to be set to, but it should definitely be there:

... lines 1 - 93
Scenario: GET a collection of battles for a programmer
... lines 95 - 99
When I request "GET /api/programmers/Fred/battles"
Then the response status code should be 200
And the "_embedded.battles" property should be an array
And the "_embedded.battles" property should contain 2 items
And the "_embedded.battles.0.didProgrammerWin" property should exist
... lines 105 - 144

Great!

This starts on line 95, so let's run this and make sure it fails with a 404:

php vendor/bin/behat features/api/programmer.feature:95

Cool!

Coding up the Programmer's Battles Endpoint

Let's get to work! Open ProgrammerController. We'll need a new route and I'll copy the "show" route, since the URL will be really similar. We'll add the /battles in the end and change the method to listBattlesAction:

... lines 1 - 19
protected function addRoutes(ControllerCollection $controllers)
{
... lines 22 - 36
$controllers->get('/api/programmers/{nickname}/battles', array($this, 'listBattlesAction'))
->bind('api_programmers_battles_list');
}
... lines 40 - 179

The route name isn't important yet, but we'll use it later to link. Let's call it api_programmers_battles_list.

Implementing this is going to be really easy! I'll put it right between showAction and listAction so I can steal from both. Ok, let's think about what we need to do. First, we need to find the Programmer for this nickname. We have code for this, so let's steal it:

... lines 1 - 90
public function listBattlesAction($nickname)
{
$programmer = $this->getProgrammerRepository()->findOneByNickname($nickname);
if (!$programmer) {
$this->throw404('Oh no! This programmer has deserted! We\'ll send a search party!');
}
... lines 98 - 105
);
... lines 107 - 179

If you find yourself repeating a lot of code like this, you can always create a private function inside your controller class and put it there. That's similar to what we've been doing by putting functions inside of BaseController.

The second thing we need to do is to find all of the battles that are linked to this programmer. I have a shortcut for this that I'll use:

... lines 1 - 90
public function listBattlesAction($nickname)
{
$programmer = $this->getProgrammerRepository()->findOneByNickname($nickname);
... lines 94 - 98
$battles = $this->getBattleRepository()
->findAllBy(array('programmerId' => $programmer->id));
... lines 101 - 110
}
... lines 112 - 179

The code might be different in your project, but this is just saying:

"Hey, go query the battle table where programmerId is equal to the id
of the programmer that we have."

So what's cool is that from here, this is exactly like the listAction, because it's just a collection resource. So I'm going to grab everything from it, change the variable to $battles, change the key to battles, and that's it!

... lines 1 - 90
public function listBattlesAction($nickname)
{
... lines 93 - 98
$battles = $this->getBattleRepository()
->findAllBy(array('programmerId' => $programmer->id));
$collection = new CollectionRepresentation(
$battles,
'battles',
'battles'
);
$response = $this->createApiResponse($collection);
return $response;
}
... lines 112 - 179

So with almost no work, we'll run the test again, and it passes!

php vendor/bin/behat features/api/programmer.feature:95

Adding the battles Relation

Back on the Hal Browser, if we hit Go, we still don't have that link. We know that we can just add /battles onto the URL, but there's no link yet. Let's add it!

Open up the Programmer class and copy and paste to create a second Relation. This time the key will be battles, and below we'll grab the route name we created for the endpoint:

... lines 1 - 7
use Hateoas\Configuration\Annotation as Hateoas;
... line 9
/**
... lines 11 - 18
* @Hateoas\Relation(
* "battles",
* href = @Hateoas\Route(
* "api_programmers_battles_list",
* parameters = { "nickname" = "expr(object.nickname)" }
* )
* )
*/
class Programmer
... lines 28 - 67

Then, everything else looks good, because this route does need the nickname. So you may want to write a test for this if it's really important, but I'm just going to go back to the Hal browser, click "Go", and boom! We have a new battles link, which we can follow and see the collection resource. We can open up one of the battles, follow a link back to the related programmer and click to go back to the battles once again. We're surfing through our API, which is really cool!

Adding the Battle self Relation

If we click to look at a battle, you'll notice that we're missing one little thing. It has a programmer link, but no self link, which we really want every resource to have because it's a nice standard and it comes in handy. So let's go add this, which is really easy.

Open the Battle class and copy the relation. Let's remove the embedded option. We just want this to be a normal link. Change the link name to self and go find the route name from inside BattleController. For this, it's api_battle_show. In this case, the route needs the id of the battle. So on the relation, we can simply say object.id:

... lines 1 - 6
use Hateoas\Configuration\Annotation as Hateoas;
... line 8
/**
* @Hateoas\Relation(
* "self",
* href = @Hateoas\Route(
* "api_battle_show",
* parameters = { "nickname" = "expr(object.id)" }
* )
* )
... lines 17 - 25
*/
class Battle
... lines 28 - 59

Awesome!

If we re-GET this request, we see a huge error! This is no bueno! But hey, let's run our test for this to see if it helps us:

php vendor/bin/behat features/api/programmer.feature:95

And you can see that we're missing some "id" parameter when generating the URL. I made a mistake in the Relation. You probably saw me do it, but I'm still passing a nickname instead of passing the id:

... lines 1 - 8
/**
* @Hateoas\Relation(
* "self",
* href = @Hateoas\Route(
* "api_battle_show",
* parameters = { "id" = "expr(object.id)" }
* )
* )
... lines 17 - 25
*/
class Battle
... lines 28 - 59

So now, things work. Thank God for our tests, because that was really easy to debug. And every battle now has that self link.

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 a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "silex/silex": "~1.0", // v1.3.2
        "symfony/twig-bridge": "~2.1", // v2.7.3
        "symfony/security": "~2.4", // v2.7.3
        "doctrine/dbal": "^2.5.4", // v2.5.4
        "monolog/monolog": "~1.7.0", // 1.7.0
        "symfony/validator": "~2.4", // v2.7.3
        "symfony/expression-language": "~2.4", // v2.7.3
        "jms/serializer": "~0.16", // 0.16.0
        "willdurand/hateoas": "~2.3" // v2.3.0
    },
    "require-dev": {
        "behat/mink": "~1.5", // v1.5.0
        "behat/mink-goutte-driver": "~1.0.9", // v1.0.9
        "behat/mink-selenium2-driver": "~1.1.1", // v1.1.1
        "behat/behat": "~2.5", // v2.5.5
        "behat/mink-extension": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~5.7.0", // 5.7.27
        "guzzle/guzzle": "~3.7" // v3.9.3
    }
}
userVoice