Buy

Adding Real Links with HATEOAS

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:

35 lines features/api/battle.feature
... 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":

35 lines features/api/battle.feature
... 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:

51 lines src/KnpU/CodeBattle/Model/Battle.php
... 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:

35 lines features/api/battle.feature
... 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:

131 lines features/api/programmer.feature
... 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!

  • 2015-01-05 weaverryan

    Hi Jake!

    Sorry for the late response due to the holidays :).

    You bring up some good questions. First, you asked about being able to update the list or projects related to a programmer. One important thing to remember (and this fooled me for a long time) is that the format that the client sends to the server to do things like updates/creates, should NOT be in some fancy hypermedia format (like HAL). This means that you can choose whatever structure you want, then document it (and of course, try to be consistent). So, I might do something like this (imagine these are docs):

    To update the projects related to a programmer, send a PUT request to /api/programmers/{nickname}/projects with a JSON body that looks like this:


    {
    "projects": [13, 14, 15]
    }

    One could argue that the JSON could *just* be the array of id's - I think it's not too important. In this case, I'm treating the projects as a subordinate resource to programmers and and updating it with a PUT.

    Next, I would probably not normally expose the "join" table to the user, but it's up to you. After-all, it may be a join table today, but perhaps tomorrow you suddenly realize that you need to add a new column to that table called "programmerRatingOfProject". If you did this, it would probably be its own resource and you wouldn't be able to do something like what I did above anymore (assuming programmerRatingOfProject is required).

    Finally, when an attribute has multiple values, which are stored in an extra table, your best bet depends on your situation. Abstracting it from your API (treating them as an array of values) is probably easier for your API client, but probably harder for you to code. So, weigh the trade-off and do what's best :).

    Cheers and apologies again for the late reply!

  • 2014-12-19 Jake

    How are many-to-many relationships represented in HAL?

    For example, a programmer can have many projects and a project can have many programmers.

    Programmer <--many to many--> Projects

    My concern is with the association table (let's call it the middle table) in between the programmer and the projects..
    Suppose I want to update the list of projects related to a programmer, should I consider the middle table as a resource and work on that resource.

    {
    name: "Foo",
    "_embedded": {
    "ProgrammerProjects": [
    {
    id: 1,
    project_id: 100
    },
    {
    id: 2,
    project_id: 102
    }
    ]
    }
    }

    Or, is it possible to make the association an attribute of the programmer resource.

    {
    name: "Foo"
    projects: [
    100,
    102
    ]
    }

    Sometimes, a resource can have an attribute that has multiple values. Thus, we use extra tables in the database to store them. Should we abstract that detail from the API. That is, just show it as an array of values and don't deal with them as separate resources?