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

Customize how your Links Render

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

As cool as all this HAL JSON stuff is, you need to build your API for whoever is using it - maybe a JavaScript frontend, a mobile app or your customers themselves. And honestly, I don't think that the standardized formats - like Hal - are all that understandable or useful. This _embedded thing? To me it's just ugly.

I also don't love hiding the URL under an object with an href key. So let's suppose that we're building a JavaScript frontend, and it'll work better if the link URL's appeared directly under the _links key - without the href.

Let's do that! The HATEOAS library we installed really just helps you add relations to a class: both link relations and embedded relations. And fortunately, the library let's you control exactly how these are added to your response.

Custom Serializer

In AppBundle, in the Serializer directory, create a new class called CustomHATEOASJsonSerializer:

... lines 1 - 2
namespace AppBundle\Serializer;
... lines 4 - 6
use Hateoas\Serializer\JsonHalSerializer;
... lines 8 - 10
class CustomHATEOASJsonSerializer extends JsonHalSerializer
{
}

Make it extend a class called JsonHalSerializer: this is the current class responsible for adding links in the HAL format. In fact: open up the class.

It has two method. serializeLinks() is responsible for reading the Relation annotations and adding them to the JSON with _links. serializeEmbeddeds() adds any embedded relations under the _embedded key.

For now, let's focus on changing how the links render only. Go to the "Code"->"Generate" menu - command+N on a Mac - and hit "Override Methods". Override serializeLinks():

... lines 1 - 7
use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\SerializationContext;
class CustomHATEOASJsonSerializer extends JsonHalSerializer
{
... lines 13 - 17
public function serializeLinks(array $links, JsonSerializationVisitor $visitor, SerializationContext $context)
{
... lines 20 - 25
}
}

Re-open the parent method and then the interface: I want to copy all that good PHPDoc so we get auto-complete. Paste it above our method and auto-complete the Link to get its use statement:

... lines 1 - 5
use Hateoas\Model\Link;
... lines 7 - 10
class CustomHATEOASJsonSerializer extends JsonHalSerializer
{
/**
* @param Link[] $links
* @param JsonSerializationVisitor $visitor
* @param SerializationContext $context
*/
public function serializeLinks(array $links, JsonSerializationVisitor $visitor, SerializationContext $context)
{
... lines 20 - 25
}
}

Alright: this should be easy.

Create a $serializedLinks array and foreach over the $links variable:

... lines 1 - 10
class CustomHATEOASJsonSerializer extends JsonHalSerializer
{
/**
* @param Link[] $links
* @param JsonSerializationVisitor $visitor
* @param SerializationContext $context
*/
public function serializeLinks(array $links, JsonSerializationVisitor $visitor, SerializationContext $context)
{
$serializedLinks = array();
foreach ($links as $link) {
... line 22
}
... lines 24 - 25
}
}

Each of these is a Link object, and contains the configuration for one annotation. Now, just create the format we want: $serializedLinks[], with $link->getRel(). Instead of setting this to an array with an href key, simply set it to $link->getHref():

... lines 1 - 10
class CustomHATEOASJsonSerializer extends JsonHalSerializer
{
/**
* @param Link[] $links
* @param JsonSerializationVisitor $visitor
* @param SerializationContext $context
*/
public function serializeLinks(array $links, JsonSerializationVisitor $visitor, SerializationContext $context)
{
$serializedLinks = array();
foreach ($links as $link) {
$serializedLinks[$link->getRel()] = $link->getHref();
}
... lines 24 - 25
}
}

Perfect! Finally, at the bottom, we need to add the _links property. Do that with: $visitor->addData('_links', $serializedLinks):

... lines 1 - 10
class CustomHATEOASJsonSerializer extends JsonHalSerializer
{
/**
* @param Link[] $links
* @param JsonSerializationVisitor $visitor
* @param SerializationContext $context
*/
public function serializeLinks(array $links, JsonSerializationVisitor $visitor, SerializationContext $context)
{
$serializedLinks = array();
foreach ($links as $link) {
$serializedLinks[$link->getRel()] = $link->getHref();
}
$visitor->addData('_links', $serializedLinks);
}
}

With any luck, that should give us a simpler format without that href.

Registering the Serializer

To hook this up. You guys can probably guess step 1: in app/config/services.yml, register this as a service. How about: custom_hateoas_json_serializer. Set its class to that same thing:

... lines 1 - 5
services:
... lines 7 - 36
custom_hateoas_json_serializer:
class: AppBundle\Serializer\CustomHATEOASJsonSerializer

And we don't have any constructor args yet.

Finally, copy the service name. To tell the bundle to use our class instead of the existing one, open up config.yml. Now, without even looking at its docs, we can get a list of the configuration for this bundle by going to the terminal and running:

./bin/console debug:config

Thanks to the bundle, there's a new valid config key called bazinga_hateoas. Pass that to the same command:

./bin/console debug:config bazinga_hateoas

Ah, that serializer.json key looks like our target.

Back in config.yml, add bazinga_hateoas, serializer, json and then paste our service name:

... lines 1 - 78
bazinga_hateoas:
serializer:
json: custom_hateoas_json_serializer

That should do it!

Changing our Tests Back

But don't run the tests quite yet: we know some things will be broken. In BattleControllerTest, take off the href we just added: it should be _links.programmer:

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTCreateBattle()
{
... lines 18 - 40
$this->asserter()->assertResponsePropertyEquals(
$response,
'_links.programmer',
$this->adjustUri('/api/programmers/Fred')
);
... lines 46 - 53
}
... lines 55 - 79
}

And in ProgrammerControllerTest, under testGETProgrammer, do the same:

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 38
public function testGETProgrammer()
{
... lines 41 - 56
$this->asserter()->assertResponsePropertyEquals(
$response,
'_links.self',
$this->adjustUri('/api/programmers/UnitTester')
);
}
... lines 63 - 289
}

Phew! That's a lot of changes, so let's re-run the entire test suite:

./vendor/bin/phpunit

Hey, it passes! I must've left a debugResponse() in there somewhere: but that's nothing to worry about - we're green!

Leave a comment!

0
Login or Register to join the conversation
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