Adding Real Links with HATEOAS

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

Let's add more links! Back in battle.feature, we're returning a programmerUri, which was our way of creating a link before we knew there was a good standard to follow:

... lines 1 - 25
Scenario: GET one battle
... lines 27 - 33
And the "programmerUri" property should equal "/api/programmers/Fred"

So now we can say: And the "_links.programmer.href" property should equal "/api/programmers/Fred":

... lines 1 - 25
Scenario: GET one battle
... lines 27 - 33
And the "_links.programmer.href" property should equal "/api/programmers/Fred"

This time, instead of using self, we're using programmer. There are some special names like self that mean something, but when you're linking from a battle to a programmer, we'll just invent something new. We'll want to use this consistently in our API: whenever we're linking to a programmer, we'll use that same string programmer so that our API clients learn that whenever they see this link link they know what type of resource to expect on the other side.

First, let's run our test - line 26 - and make sure that it fails:

php vendor/bin/behat features/api/battle.feature:26

Let's go in and add that relation. Open up Battle and also open up Programmer so we can steal the Relation from there as promised. And don't forget, every time you use an annotation for the first time in a class, you need a use statement for it.

And also, since we have this relationship now, I'm going to remove our VirtualProperty down below. So this is really good - we're linking to a Programmer like before. So the route name is good and the nickname is good. The only thing that needs to change is that in order to get the nickname of the programmer for this Battle, we need to say object.programmer.nickname so that it uses the programmer field below. Let's try our test. Ah, and it fails! I got caught by copying and pasting - we do have a link, but its name is self. Change that to be programmer:

... lines 1 - 6
use Hateoas\Configuration\Annotation as Hateoas;
... line 8
* @Hateoas\Relation(
* "programmer",
* href = @Hateoas\Route(
* "api_programmers_show",
* parameters = { "nickname" = "expr(object.programmer.nickname)" }
* )
* )
* @Serializer\ExclusionPolicy("all")
class Battle
... lines 20 - 51

And now, we'll get that to pass. Awesome.

Consistency = New Behat Step

Because we're always putting links under a _links key, I have a new piece of language that we can use in Behat to check for links:

... lines 1 - 25
Scenario: GET one battle
... lines 27 - 33
And the link "programmer" should exist and its value should be "/api/programmers/Fred"

Why would I do this? It's just proving how consistent we are. This new sentence will look for the _links property, so there's no reason to repeat it in all of our scenarios. So let's try the test again - perfect.

php vendor/bin/behat features/api/battle.feature:26

We can repeat this same thing over in programmer.feature when we're checking the self link. I'll comment out the old line for reference:

... lines 1 - 65
Scenario: GET one programmer
... lines 67 - 80
And the link "self" should exist and its value should be "/api/programmers/UnitTester"
... lines 82 - 131

If we run our entire test suite, things keep passing:

php vendor/bin/behat

I love to see all of that green!

Leave a comment!

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