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 SubscribeWe just added a link to our API endpoint for getting a single battle. I want to see the response again, so let's add "And print last response" then run the test - it starts on line 26.
vendor/bin/behat features/api/battle.feature:26
We just invented this idea to put a field on a link called programmerUri
. Part of the issue is that we have this link mixed up with other real data fields on this property. It's not totally obvious if we can follow this link, or if maybe this is just a field that happens to be a URL, and that we could actually change that URL by sending a PUT request if we wanted to.
A lot of smart people have thought about this and have invented different standardized formats for how your data and links should be organized inside of JSON or XML. One popular one right now is called HAL. I'll click into a document that has a really nice example:
{
"_links": {
"self": {
"href": "http://example.org/api/user/matthew"
}
}
"id": "matthew",
"name": "Matthew Weier O'Phinney"
}
You can see that down here is the data, and above that, you have a _links
property that holds the links. You'll also notice that there's this link called self
, and that's something we're going to see over and over again. self
is kind of a standard thing where each resource has a link to itself, and you keep that on a key called self
. What's cool about this is that it's used on a lot of APIs. So if you ever see a link with the key self
, you already know what they're talking about. We're going to add this to our stuff too.
Right now, we're using the serializer to create our JSON. And it works just by looking at the class of whatever object we're serializing and grabs the properties off of it. And of course we have some control over which properties to use.
But if you look at the _links.self
thing, you might be wondering how we're going to do this. Are we going to need a bunch of these VirtualProperty
things for _links
?
Fortunately, there's a really nice library that integrates with the serializer and helps us add links. It's called HATEOAS, which is a REST term that we'll talk about later.
Before we look at how this library works, let's get it installed. Copy the name, then run composer require
and the library name:
composer require willdurand/hateoas
Composer will figure out the best version to bring into the project.
Tip
Since the latest version of willdurand/hateoas
is incompatible with some of our dependencies,
double-check that you got installed the version ^2.3
Just like the serializer, this library works via annotations:
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Hateoas\Relation("self", href = "expr('/api/users/' ~ object.getId())")
*/
class User
{
private $id;
private $firstName;
private $lastName;
public function getId() {}
}
It basically says the User
class has a relation called self
, and its href
should be set to /api/users
, and then the id
of the user. And we'll talk about this syntax in a second. The end result will be something that looks like this:
{
"_links": {
"self": {
"href": "/api/users/12"
}
}
"id": "12",
"firstName": "Leanna",
"lastName": "Pelham"
}
It'll create the _links
key with self
and any other links you add below it.
Getting this setup is pretty easy. Find the HateoasBuilder
code and copy it. Quickly, I'll make sure Composer is done downloading the library.
There's always a place inside a Silex application - and most frameworks - to define services, which are just re-usable objects. You might remember from earlier, that in my project, this is done inside this Application
class. A few chapters ago, we used this to configure the serializer
object. The HATEOAS library hooks right into this, so we just need to modify a few things. Instead of returning the serializer, we'll set it to a variable and take off the call to build()
. Now we'll paste the new code in, return it, and pass the $serializerBuilder
into the create()
method, which is what ties the two libraries together:
... lines 1 - 5 | |
use Hateoas\HateoasBuilder; | |
... lines 7 - 149 | |
private function configureServices() | |
{ | |
$app = $this; | |
... lines 153 - 216 | |
$this['serializer'] = $this->share(function() use ($app) { | |
// configure the underlying serializer | |
$jmsBuilder = \JMS\Serializer\SerializerBuilder::create() | |
->setCacheDir($app['root_dir'].'/cache/serializer') | |
->setDebug($app['debug']) | |
->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy()); | |
// create the Hateoas serializer | |
return HateoasBuilder::create($jmsBuilder)->build(); | |
}); | |
... lines 227 - 230 | |
} | |
... lines 232 - 349 |
Now of course, my editor is angry because I'm missing my use
statement, so I'll use a shortcut to add that, which puts use
statement at the top of this file.
So that's it. We're already using the serializer
object everywhere, and now the HATEOAS library will be working with that to add these links for us.
Before we add our first link, let's add a scenario to look for it. Go into programmer.feature
and find the scenario for getting one programmer. As I mentioned before. it's always a really good idea to have a self
link. And if we look at the structure of HAL, this means we're going to have a _links.self.href
property, and it'll be set to the URI of our programmer.
So let's add that here: And the "_links.self.href" property should equal
- and we know what the URL of this is going to be - /api/programmers/UnitTester
:
... lines 1 - 65 | |
Scenario: GET one programmer | |
... lines 67 - 69 | |
When I request "GET /api/programmers/UnitTester" | |
... lines 71 - 78 | |
And the "userId" property should not exist | |
And the "nickname" property should equal "UnitTester" | |
And the "_links.self.href" property should equal "/api/programmers/UnitTester" | |
... lines 82 - 131 |
And as always, let's run this to see it fail. This scenario is on line 66 of programmer.feature
:
vendor/bin/behat features/api/programmer.feature:66
And it fails because the property doesn't even exist yet.
Go back to the HATEOAS docs and scroll back up. Grab the use
statement and put it inside of the Programmer
class. I'll go back and copy the HATEOAS\Relation
annotation - beautiful. This says self
, because we want this to be the self
link and we'll change the href to be /api/programmers/
and then object.nickname
:
... lines 1 - 7 | |
use Hateoas\Configuration\Annotation as Hateoas; | |
... line 9 | |
/** | |
* @Serializer\ExclusionPolicy("all") | |
* @Hateoas\Relation("self", href = "expr('/api/programmers/' ~ object.nickname)") | |
*/ | |
class Programmer | |
{ | |
... lines 16 - 52 | |
} |
Now, what the heck is this expr
thing? This comes from Symfony's expression language, which is a small library that gives you a mini, PHP-like language for writing simple expressions. It has things like strings, numbers and variables. There are also functions and operators - very similar to PHP, but with some different syntax. You only have access to a few variables and a few functions, so you're sandboxed a bit.
In this case, we're saying the URL is going to be this string, then the tilde (~
) is the concatenation character, so like the dot (.
) in PHP. After that, we have object.nickname
. When you use the HATEOAS\Relation
, it takes whatever object you're serializing - so Programmer
in this case - and makes it available as a variable in the expression called object
. So by saying object.nickname
, we're saying go get the nickname
property.
Let's try this test!
vendor/bin/behat features/api/programmer.feature:66
Awesome, it passes that easily. Let's print out the response temporarily. And you can see that we do have that _links
property with self
and href
keys inside of it. That transformation is all being taken care of by the HATEOAS library.
Hey abyssweb!
Ah, I see it! This tutorial is starting to show its age (the concepts are still great, but the code is getting old). When this tutorial was recorded, running this composer require command installed version 2.3 of the library. Now it installed version 3.1, which is incompatible (as you saw) with some other stuff we have installed (particularly our version of the expression language). To follow the tutorial exactly, try this instead:
composer require willdurand/hateoas:~2.3
We'll add a note about it :).
Cheers!
I'm not used to Disqus, and I edited too much my precedent reply.
Well I was able to install HATEOAS v2.3, I had to downgrade JMSSerializer to do so. I copied the composer.json from the finish course code.
Now, even when using only the finish course code, the battle.feature doesn't work anymore, I have a big PHP error block (using XDebug isn't a great idea when working on API I think).
I will follow the rest of the course only on video. The principles are still good so, it will still be useful ! Thank you !
Hi ! Thank you for your answer.
Now it seems like I have an issue with my version for JMSSerializer :
` Problem 1
- willdurand/hateoas v2.9.0 requires jms/serializer ~1.0 -> satisfiable by jms/serializer[1.0.0, 1.1.0, 1.10.0, 1.11.0, 1.12.0, 1.12.1
, 1.13.0, 1.14.0, 1.2.0, 1.3.0, 1.3.1, 1.4.0, 1.4.1, 1.4.2, 1.5.0, 1.5.0-RC1, 1.6.0, 1.6.0-RC1, 1.6.1, 1.6.2, 1.7.0, 1.7.0-RC1, 1.7.0-RC2,
1.7.1, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 1.9.2, 1.x-dev] but these conflict with your requirements or minimum-stability.
- willdurand/hateoas v2.8.1 requires jms/serializer ~1.0 -> satisfiable by jms/serializer[1.0.0, 1.1.0, 1.10.0, 1.11.0, 1.12.0, 1.12.1
, 1.13.0, 1.14.0, 1.2.0, 1.3.0, 1.3.1, 1.4.0, 1.4.1, 1.4.2, 1.5.0, 1.5.0-RC1, 1.6.0, 1.6.0-RC1, 1.6.1, 1.6.2, 1.7.0, 1.7.0-RC1, 1.7.0-RC2,
1.7.1, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 1.9.2, 1.x-dev] but these conflict with your requirements or minimum-stability.
- willdurand/hateoas v2.8.0 requires jms/serializer ~1.0 -> satisfiable by jms/serializer[1.0.0, 1.1.0, 1.10.0, 1.11.0, 1.12.0, 1.12.1
, 1.13.0, 1.14.0, 1.2.0, 1.3.0, 1.3.1, 1.4.0, 1.4.1, 1.4.2, 1.5.0, 1.5.0-RC1, 1.6.0, 1.6.0-RC1, 1.6.1, 1.6.2, 1.7.0, 1.7.0-RC1, 1.7.0-RC2,
1.7.1, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 1.9.2, 1.x-dev] but these conflict with your requirements or minimum-stability.
- willdurand/hateoas v2.7.0 requires jms/serializer ~1.0 -> satisfiable by jms/serializer[1.0.0, 1.1.0, 1.10.0, 1.11.0, 1.12.0, 1.12.1
, 1.13.0, 1.14.0, 1.2.0, 1.3.0, 1.3.1, 1.4.0, 1.4.1, 1.4.2, 1.5.0, 1.5.0-RC1, 1.6.0, 1.6.0-RC1, 1.6.1, 1.6.2, 1.7.0, 1.7.0-RC1, 1.7.0-RC2,
1.7.1, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 1.9.2, 1.x-dev] but these conflict with your requirements or minimum-stability.
- willdurand/hateoas v2.6.0 requires jms/serializer ~0.13 -> satisfiable by jms/serializer[0.13.0, 0.14.0, 0.15.0, 0.16.0] but these c
onflict with your requirements or minimum-stability.
- willdurand/hateoas v2.5.0 requires jms/serializer ~0.13@dev -> satisfiable by jms/serializer[0.13.0, 0.14.0, 0.15.0, 0.16.0] but the
se conflict with your requirements or minimum-stability.
- willdurand/hateoas v2.4.0 requires jms/serializer ~0.13@dev -> satisfiable by jms/serializer[0.13.0, 0.14.0, 0.15.0, 0.16.0] but the
se conflict with your requirements or minimum-stability.
- willdurand/hateoas v2.3.0 requires jms/serializer ~0.13@dev -> satisfiable by jms/serializer[0.13.0, 0.14.0, 0.15.0, 0.16.0] but the
se conflict with your requirements or minimum-stability.
- willdurand/hateoas 2.9.1 requires jms/serializer ~1.0 -> satisfiable by jms/serializer[1.0.0, 1.1.0, 1.10.0, 1.11.0, 1.12.0, 1.12.1,
1.13.0, 1.14.0, 1.2.0, 1.3.0, 1.3.1, 1.4.0, 1.4.1, 1.4.2, 1.5.0, 1.5.0-RC1, 1.6.0, 1.6.0-RC1, 1.6.1, 1.6.2, 1.7.0, 1.7.0-RC1, 1.7.0-RC2,
1.7.1, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 1.9.2, 1.x-dev] but these conflict with your requirements or minimum-stability.
- willdurand/hateoas 2.12.0 requires jms/serializer ^1.7 -> satisfiable by jms/serializer[1.10.0, 1.11.0, 1.12.0, 1.12.1, 1.13.0, 1.14
.0, 1.7.0, 1.7.0-RC1, 1.7.0-RC2, 1.7.1, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 1.9.2, 1.x-dev] but these conflict with your requirements or minimum-s
tability.
- willdurand/hateoas 2.11.0 requires jms/serializer ^1.7 -> satisfiable by jms/serializer[1.10.0, 1.11.0, 1.12.0, 1.12.1, 1.13.0, 1.14
.0, 1.7.0, 1.7.0-RC1, 1.7.0-RC2, 1.7.1, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 1.9.2, 1.x-dev] but these conflict with your requirements or minimum-s
tability.
- willdurand/hateoas 2.10.0 requires jms/serializer ~1.0 -> satisfiable by jms/serializer[1.0.0, 1.1.0, 1.10.0, 1.11.0, 1.12.0, 1.12.1
, 1.13.0, 1.14.0, 1.2.0, 1.3.0, 1.3.1, 1.4.0, 1.4.1, 1.4.2, 1.5.0, 1.5.0-RC1, 1.6.0, 1.6.0-RC1, 1.6.1, 1.6.2, 1.7.0, 1.7.0-RC1, 1.7.0-RC2,
1.7.1, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 1.9.2, 1.x-dev] but these conflict with your requirements or minimum-stability.
- Installation request for willdurand/hateoas ~2.3 -> satisfiable by willdurand/hateoas[2.10.0, 2.11.0, 2.12.0, 2.9.1, v2.3.0, v2.4.0,
v2.5.0, v2.6.0, v2.7.0, v2.8.0, v2.8.1, v2.9.0]. `
I will try to downgrade my JMS version so it can satisfy the HATEOAS version.
Edit : I downgraded in my composer.json the JMSSerializer from ^3.1 to ~1.0 and I could install HATEOAS v2.3. Thank you !
Edit 2: Even by copying the composer.json from the finish course code, now my tests don't pass anymore, I have a PHP error concerning JMSSerializer.
Hey Virginie,
If you used to use a more fresh version of JMSSerializer like 3.1 before and then downgraded to the 1.0 - it makes sense, your tests might be broken due to it because of BC breaks. It sounds like you have to fix using of JMSSerializer in some places - use it in v1.0 way.
I hope it helps!
Cheers!
I managed to find a suitable configuration to finish the course. I downgraded everything, even my PHP version, and I could follow the video while practicing.
Thank you for your answer !
Hey Virginie,
I'm glad you were able to continue the course. It's good to download the course code and code along with us, this way you will have the exact code we have in our screencasts. But if you want to apply this code in your own project - it might be tricky sometimes.
Cheers!
Yo Flavius Andrei!
Great question :). I sort of... don't have one. It seems this battle is being fought still, and ultimately, the usefulness of these standard formats is somewhat limited (it'll be really useful when more clients know how to automatically read the different formats). Plus, sometimes creating the responses in the different formats can be laborious, and often I find myself a bit frustrated by the formats (they look "busy" to me).
That being said, it does seem like Hal isn't getting as much love as some other formats right now. And ultimately, I just want to pick the winner :). So if JSON-LD feels good to you, and you have some libraries to help you output it, go for it. It's definitely a solid format. Just remember who your API clients are (either you, or if you have real API clients that will use your API) - make sure it works for them!
Cheers!
well, i was going with json-ld until i saw this video :) now i switched to hal. i've never paid any attention to it up until the detailed explanation i found here. now, i love it :)
thanks and keep up the awesome work!
// 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
}
}
Hi !
I have trouble installing HATEOAS here.
First, the provided link doesn't work anymore, I get an blank page.
Secondly, when trying to install it via Composer, I have the following issue :
`Your requirements could not be resolved to an installable set of packages.
Problem 1
Do you know how I could solve the issue ? It seems that I have a version conflict.