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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeGoogle 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 withhref
. 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 anhref
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.
Updating Programmer to use the new Links
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.
If I understand correctly, using Hateoas annotations no longer requires parameter resolution in LinkSerializationSubscriber::onPostSerialize()?
I used the LinkSerializationSubscriber::onPostSerialize() method to dynamically inject some parameters from the current Request, using $this->request->getCurrentRequest() into your @Link, but don't know how to do the same for @Hateoas\Relation. How can I dynamically inject some parameters (from the current Request) into a Hateoas annotation before it processes/resolves its parameters?
I tried doing an elseif ($annotation instanceof Hateoas\Relation) { and inject them there, but this doesn't work, probably because the $this->annotationReader->getClassAnnotations returns a copy of annotations, and not their references which can be modified prior to processing.
Thank you!