This tutorial has a new version, check it out!

The HAL JSON Standard

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

Google for "How to remove a mustard stain from a white shirt". I mean, Google for "HAL JSON" - sorry, it's after lunch.

This is one of a few competing hypermedia formats. And remember, hypermedia is one of our favorite buzzwords: it's a media type, or format, - like JSON - plus some rules about how you should semantically organize things inside that format. In human speak, HAL JSON says:

Hi I'm HAL! If you want to embed links in your JSON, you should put them under an _links key and point to the URL with href. Have a lovely day!

If you think about it, this idea is similar to HTML. In HTML, there's the XML-like format, but then there are rules that say:

Hi, I'm HTML! If you want a link, put it in an <a> tag under an href attribute.

The advantage of having standards is that - since the entire Internet follows them - we can create a browser that understands the significance of the <a> tag, and renders them clickable. In theory, if all API's followed a standard, we could create clients that easily deal with the data.

So let's also update the Programmer entity to use the new system. Copy the whole @Relation from Battle:

... lines 1 - 10
/**
... lines 12 - 14
* @Hateoas\Relation(
* "programmer",
* href=@Hateoas\Route(
* "api_programmers_show",
* parameters={"nickname"= "expr(object.getProgrammerNickname())"}
* )
* )
... line 22
class Battle
... lines 24 - 141

And replace the @Link inside of Programmer. Change the rel back to self and update the expression to object.getNickname():

... lines 1 - 8
use Hateoas\Configuration\Annotation as Hateoas;
/**
... lines 12 - 16
* @Hateoas\Relation(
* "self",
* href=@Hateoas\Route(
* "api_programmers_show",
* parameters = { "nickname"= "expr(object.getNickname())" }
* )
* )
*/
class Programmer
... lines 26 - 201

Make sure you've got all your parenthesis in place. Oh, and don't forget to bring over the use statement from Battle.

In ProgrammerControllerTest, the testGETProgrammer method looks for _links.self:

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 37
public function testGETProgrammer()
{
... lines 40 - 55
$this->asserter()->assertResponsePropertyEquals(
... line 57
'_links.self',
... line 59
);
}
... lines 62 - 288
}

Add .href to this to match the new format:

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 37
public function testGETProgrammer()
{
... lines 40 - 55
$this->asserter()->assertResponsePropertyEquals(
... line 57
'_links.self.href',
... line 59
);
}
... lines 62 - 288
}

Try it out!

vendor/bin/phpunit --filter testGETProgrammer

Yes!

Should I Use HAL JSON?

So why use a standardized format like Hal? Because now, we can say:

Hey, our API returns HAL JSON responses!

Then, they can go read its documentation to find out what it looks like. Or better, they might already be familiar with it!

Advertising that you're using Hal

So now that we are using Hal, we should advertise it! In fact, that's what this application/hal+json means in their documentation: it's a custom Content-Type. It means that the format is JSON, but there's some extra rules called Hal. If a client sees this, they can Google for it.

In ProgrammerControllerTest, assert that application/hal+json is equal to $response->getHeader('Content-Type')[0]:

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTProgrammerWorks()
{
... lines 18 - 30
$this->assertEquals('application/hal+json', $response->getHeader('Content-Type')[0]);
... lines 32 - 36
}
... lines 38 - 289
}

Guzzle returns an array for each header - there's a reason for that, but yea, I know it looks ugly.

To actually advertise that our API returns HAL, open BaseController and search for createApiResponse() - the method we're calling at the bottom of every controller. Change the header to be application/hal+json:

... lines 1 - 19
abstract class BaseController extends Controller
{
... lines 22 - 117
protected function createApiResponse($data, $statusCode = 200)
{
$json = $this->serialize($data);
return new Response($json, $statusCode, array(
'Content-Type' => 'application/hal+json'
));
}
... lines 126 - 185
}

Nice! Copy the test name and re-run the test:

./vendor/bin/phpunit --filter testPOSTProgrammerWorks

Congratulations! Your API is no longer an island: welcome to the club.

Leave a comment!