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.
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.
Hey Vlad!
You're absolutely right - LinkSerializationSubscriber is no longer needed. Were you passing some extra request information into the expression before? What exactly were you doing? The Hateoas library *tries* to give you access to everything you'd need. There's even a few more things that the bundle gives you access to: https://github.com/willdura...
But let me know, and we'll find a solution :)
Cheers!
Hi Ryan!
Yes, I'm trying to get a specific parameter from the Request object. In my case the route is: /api/schools/{schoolId}. The `schoolId` parameter is needed to build the links (_self, next, previous) in the `_links` section. With LinkSerializationSubscriber I was able to get the current Request ($this->request->getCurrentRequest()) in the onPostSerialize() method and extract the `schoolId` from it, but since Hateoas bypasses LinkSerializationSubscriber, I'm not sure how to do it there.
You've already shown me how to get the current request from a controller or a service, but I don't know how to get it from a Hateoas annotation.
Thank you for your reply.
Hey Vlad!
Ok, great! So, the first question is: inside what entity/class are you adding this annotation? If, for example, you are inside of the School entity, then you can use the expression language to get the schoolId value without doing any extra work. It would look something like this:
/**
* @Hateoas\Relation(
* "self",
* href = @Hateoas\Route(
* "school_show",
* parameters = {
* "shoolId" = "expr(object.getId())"
* }
* )
* )
*/
class School
So, you don't really need to fetch this schoolId from the request directly. Ultimately, you are serializing an object, and when you do that, it is highly likely that the value you need - the school id - is contained inside of that object somewhere (and so you can fetch it with the expression).
Let me know if this makes sense!
Hi Ryan,
Unfortunately, this isn't just for the School entity, but for pretty much all exposed entities, since my end point URLs depend upon the schoolId parameter: /api/schools/{schoolId}, /api/schools/{schoolId}/students, /api/schools/{schoolId}/faculty, etc.
Inside controllers' actions I'm able to get the schoolId from the Request parameter, like you've explained in the tutorial.
Thank you!
Hi Vlad!
Hmm, I would still expect each object that's being serialized to ultimately have a relation back to the School object somehow (e.g. the Faculty would have a ManyToOne with School, so you could do something like object.getSchool().getId() in the expression). But, let's suppose that's not true, and we need to do it this other way :). A few suggestions:
1) Based on what I just saw in the code, you have a container variable in the expressions. It's ugly, but you could do `container.get('request_stack').getCurrentRequest().attributes.get('schoolId')
2) You can add a custom function to the expression (e.g. getSchoolId()) that does whatever you want. Just create a service and tag it with hateoas.expression_function. As far as I can see, this is an undocumented feature (it's quite advanced). Here is the code that handles it: https://github.com/willdura...
As you can see, for (2), your class must implement am ExpressionFunctionInterface.
I hope this helps!
Hi Ryan!
Thanks to the HATEOAS samples and your wonderful suggestions, I was able to get it to work!
This is great! I'm really excited.
I'll paste in my code here, in case anyone else decides to do something similar:
<strong>SchoolExpressionFunction.php</strong>
/**
* Created by IntelliJ IDEA.
* User: vlad
* Date: 6/22/16
* Time: 9:30 AM
*/
namespace AppBundle\Hateoas;
use Closure;
use Hateoas\Expression\ExpressionFunctionInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class SchoolExpressionFunction implements ExpressionFunctionInterface
{
/**
* @var RequestStack
*/
private $request;
/**
* RequestExpressionFunction constructor.
*
* @param RequestStack $request
*/
public function __construct(RequestStack $request)
{
$this->request = $request;
}
/**
* Return the name of the function in an expression.
*
* @return string
*/
public function getName()
{
return 'getIdSchool';
}
/**
* Return a function executed when compiling an expression using the function.
*
* @return closure
*/
public function getCompiler()
{
return function () {
return sprintf('$school_helper->getIdSchool()');
};
}
/**
* Return a function executed when the expression is evaluated.
*
* @return closure
*/
public function getEvaluator()
{
return function (array $context) {
return $context['school_helper']->getIdSchool();
};
}
/**
* Return context variables as an array.
*
* @return array
*/
public function getContextVariables()
{
return array('school_helper' => $this);
}
/**
* Extracts and returns 'idSchool' from the current request
*
* @return string
*/
protected function getIdSchool()
{
return $this->request->getCurrentRequest()->get('idSchool');
}
}
Register as a service in <strong>services.yml</strong>
# Custom expression function to extract School ID from the current Request
school_expression_function:
class: AppBundle\Hateoas\SchoolExpressionFunction
arguments: ['@request_stack']
tags:
- { name: hateoas.expression_function }
Use in Hateoas annotations (e.g. <strong>Student</strong> entity)
/**
*
* @Hateoas\Relation(
* "self",
* href=@Hateoas\Route(
* "api_students_show",
* parameters = { "idStudent" = "expr(object.getIdStudent())", "idSchool" = "expr(getIdSchool())" }
* )
* )
*/
My setup is a bit different, as I have entities split in two different databases, hence it wasn't easy to get the school ID for some entities using relationships.
Thank you again for your help, I really appreciate it!
Hi Ryan,
I posted a comment here yesterday to let you know I got it work, as well as code samples for others interested in the same thing, but the comment was marked as pending for review. I don't know if you need to approve it first.
Thank you!
Hey, Vlad!
Sorry, looks like this comment somehow didn't pass Disqus spam filter. I've just approved it!
Thanks for let us know.
Cheers!
// 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
}
}
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!