If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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 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.
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!
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!
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.
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.0.*", // v3.0.3
"doctrine/orm": "^2.5", // v2.5.4
"doctrine/doctrine-bundle": "^1.6", // 1.6.2
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // v2.10.0
"sensio/distribution-bundle": "^5.0", // v5.0.4
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
"incenteev/composer-parameter-handler": "~2.0", // v2.1.2
"jms/serializer-bundle": "^1.1.0", // 1.1.0
"white-october/pagerfanta-bundle": "^1.0", // v1.0.5
"lexik/jwt-authentication-bundle": "^1.4", // v1.4.3
"willdurand/hateoas-bundle": "^1.1" // 1.1.1
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.6
"symfony/phpunit-bridge": "^3.0", // v3.0.3
"behat/behat": "~3.1@dev", // dev-master
"behat/mink-extension": "~2.2.0", // v2.2
"behat/mink-goutte-driver": "~1.2.0", // v1.2.1
"behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
"phpunit/phpunit": "~4.6.0", // 4.6.10
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}