Buy

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 with href. 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 an href 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:

141 lines src/AppBundle/Entity/Battle.php
... 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():

201 lines src/AppBundle/Entity/Programmer.php
... 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:

290 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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:

290 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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]:

291 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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:

187 lines src/AppBundle/Controller/BaseController.php
... 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.

Leave a comment!

  • 2016-06-24 weaverryan

    Awesome work - congrats!

  • 2016-06-23 Vlad

    Thank you, Victor!

  • 2016-06-23 Victor Bocharsky

    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!

  • 2016-06-23 Vlad

    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!

  • 2016-06-22 Vlad

    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:

    SchoolExpressionFunction.php


    /**
    * 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 services.yml


    # 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. Student 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!

  • 2016-06-21 weaverryan

    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/willdurand/...

    As you can see, for (2), your class must implement am ExpressionFunctionInterface.

    I hope this helps!

  • 2016-06-20 Vlad

    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!

  • 2016-06-18 weaverryan

    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!

  • 2016-06-16 Vlad

    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.

  • 2016-06-16 weaverryan

    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/willdurand/...

    But let me know, and we'll find a solution :)

    Cheers!

  • 2016-06-15 Vlad

    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!