Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

The HAL JSON Standard

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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:

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

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]:

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

Leave a comment!

11
Login or Register to join the conversation
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | posted 5 years ago

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!

Reply

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!

Reply
Vladimir Z. Avatar

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.

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!

Reply
Vladimir Z. Avatar

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!

Reply

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!

Reply
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | weaverryan | posted 5 years ago | edited

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!

Reply

Awesome work - congrats!

Reply
Vladimir Z. Avatar

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!

Reply

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!

Reply
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | Victor | posted 5 years ago

Thank you, Victor!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony. The concepts of Hypermedia & HATEOAS are still valid. But I recommend using API Platform in modern Symfony apps.

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice