If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
It's time to talk about a big term in REST -- hypermedia. It's one of those
terms that seems like it was invented to scare people, but it's really quite
underwhelming. We all know that every response has a Content-Type
, like
text/html
or application/json
. So when you hear "media" or "media types"
and "content types", they're referring to the same idea.
We have two things: media and we have hypermedia.
Media is any format: text/html
, text/plain
, application/json
.
These all contain data, and it's as simple as that.
Some of these formats are also called hypermedia. What's the difference between media and hypermedia? Hypermedia is a format that has a place for links to live. And that's it. The classic case you'll hear people talk about for hypermedia is HTML. HTML is the o.g. (original) hypermedia format. It contains data - like we see here - but it also has a way to express links, via anchor tags and forms are also a type of link. So these are links here and these are links. So implicit in the HTML format is a way to separate links from the rest of your data.
JSON is not hypermedia. That may seem confusing, because you might be thinking:
"but didn't we just add links to our JSON - isn't that hypermedia?"
And that answer is no, because if you read the official JSON specification, all it will talk about is where your curly braces, quotes, colons and commas should go. JSON is about the structure of the data - it says nothing about what's actually inside of the data. So by itself, JSON is just a media type, because there's nothing in there that says where your links should live.
But what's cool is that we've adopted this HAL JSON. This is something that's
built on top of JSON: it starts with the JSON structure and then adds extra
rules about where links should live. So when you talk about JSON, that's
a media format. But when you talk about HAL, that's a hypermedia format, because
it's spec tells you that links live below _links
.
So that's really it: hypermedia is just a way to say: I have a structure that returns links, and there are rules about where those links live.
As soon as you adopt a hypermedia format, instead of returning a Content-Type
of application/json
, you can return a Content-Type
of something different,
like application/hal+json
. At the bottom of this page,
there's an example of what a response looks like:
HTTP/1.1 201 Created
Content-Type: application/hal+json
Location: http://example.org/api/user/matthew
{
...
}
And you can see that the response comes back with a Content-Type
of
application/hal+json
. This is a signal to the client that the response has
a JSON structure but has some additional semantic rules on top of it. What's
awesome is that if we return this in our API and someone looks at that header,
they're going to say:
"Oh, what's this application/hal+json format?".
If they haven't heard of it, they can Google it and read about the structure and say:
"oh, they're using a format where the links live in an `_links` key"
along with some other rules.
Because we're already following HAL, returning this Content-Type
header
on all of our endpoints is an easy win. In battle.feature
, let's add
a new scenario line to test this. My editor isn't happy with my language here.
If you can't remember your definitions, run behat with the -dl
option:
php vendor/bin/behat -dl
I'll grep this for header
because I know I have a definition. Ah, and
my language is slightly off. And now PHPStorm is very happy:
... lines 1 - 25 | |
Scenario: GET one battle | |
... lines 27 - 29 | |
When I request "GET /api/battles/%battles.last.id%" | |
Then the response status code should be 200 | |
... lines 32 - 34 | |
And the "Content-Type" header should be "application/hal+json" |
Oh, and we actually want to look for application/hal+json
. I'll run the
test first, and it's failing:
php vendor/bin/behat features/api/battle.feature:26
Remember, this is served from BattleController
, so let's go back there.
And all of our endpoints call this same createApiResponse
method:
... lines 1 - 52 | |
public function showAction($id) | |
{ | |
$battle = $this->getBattleRepository()->find($id); | |
... lines 56 - 59 | |
$response = $this->createApiResponse($battle, 200); | |
return $response; | |
} | |
... lines 64 - 65 |
If we click into this, it opens up the BaseController
and this is a method
we created earlier. It uses the serializer then creates a Response
. So
let's just update that Content-Type
header:
... lines 1 - 236 | |
protected function createApiResponse($data, $statusCode = 200) | |
{ | |
$json = $this->serialize($data); | |
return new Response($json, $statusCode, array( | |
'Content-Type' => 'application/hal+json' | |
)); | |
} | |
... lines 245 - 301 |
Run the test, and it passes perfectly:
php vendor/bin/behat features/api/battle.feature:26
Now, API clients can see this header and know that we're using some extra rules on top of the JSON structure.
Hey Shaun T.!
Ah, I think it's an easy fix :). For some reason, I don't see your api.response_factory
defined: we created it here: https://knpuniversity.com/screencast/rest-ep2/centralizing-error-response-creation#creating-an-api-response-factory-service. Did you maybe just miss that part somehow?
Cheers!
// 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
}
}
Hey Ryan,
This strange error has just popped up and I can't figure out where it's coming from, any idea what's happening here?!
POST: http://localhost:8000/api/tokens
Fatal error: Uncaught InvalidArgumentException: Identifier "api.response_factory" is not defined. in /Users/shaunthornburgh/Documents/Development/code-battle-3/vendor/pimple/pimple/lib/Pimple.php:78
Stack trace:
#0 /Users/shaunthornburgh/Documents/Development/code-battle-3/src/KnpU/CodeBattle/Application.php(286): Pimple->offsetGet('api.response_fa...')
#1 /Users/shaunthornburgh/Documents/Development/code-battle-3/vendor/pimple/pimple/lib/Pimple.php(126): KnpU\CodeBattle\Application->KnpU\CodeBattle\{closure}(Object(KnpU\CodeBattle\Application))
#2 /Users/shaunthornburgh/Documents/Development/code-battle-3/vendor/pimple/pimple/lib/Pimple.php(83): Pimple::{closure}(Object(KnpU\CodeBattle\Application))
#3 /Users/shaunthornburgh/Documents/Development/code-battle-3/vendor/silex/silex/src/Silex/Provider/SecurityServiceProvider.php(399): Pimple->offsetGet('security.entry_...')
#4 /Users/shaunthornburgh/Documents/Development/code-battle-3/vendor/pimple/pimple/lib/Pimple.php(126): Silex\Provider\SecurityServiceProvider->Silex\Pro in /Users/shaunthornburgh/Documents/Development/code-battle-3/vendor/pimple/pimple/lib/Pimple.php on line 78
Here is my repo... https://github.com/shauntho...