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

Customizing (making less ugly) Embeddeds!

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

Let me show you something else I don't really like. In BattleControllerTest, we're checking for the embedded programmer. Right now it's hidden under this _embedded key:

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTCreateBattle()
{
... lines 18 - 45
$this->asserter()->assertResponsePropertyEquals(
$response,
'_embedded.programmer.nickname',
'Fred'
);
... lines 51 - 53
}
... lines 55 - 79
}

Hal does this so that a client knows which data is for the Battle, and which data is for the embedded programmer. But what if it would be more convenient for our client if the data was not under an _embedded key? What if they want the data on the root of the object like it was before?

Well, that's fine! Just stop using the embedded functionality from the bundle. Delete the assert that looks for the string and instead assert that the programmer.nickname is equal to Fred:

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTCreateBattle()
{
... lines 18 - 43
$this->asserter()->assertResponsePropertyEquals(
$response,
'programmer.nickname',
'Fred'
);
... lines 49 - 51
}
... lines 53 - 77
}

In other words, I want to change the root programmer key from a string to the whole object. And we'll eliminate the _embedded key entirely.

In Battle.php, remove the embedded key from the annotation:

... lines 1 - 10
/**
... lines 12 - 14
* @Hateoas\Relation(
* "programmer",
* href=@Hateoas\Route(
* "api_programmers_show",
* parameters={"nickname"= "expr(object.getProgrammerNickname())"}
* ),
... line 21
* )
*/
class Battle
... lines 25 - 139

OK, _embedded is gone! Next, on the programmer property, add @Expose:

... lines 1 - 23
class Battle
{
... lines 26 - 33
/**
... lines 35 - 36
* @Serializer\Expose()
*/
private $programmer;
... lines 40 - 137
}

The serializer will serialize that whole object. We originally didn't expose that property because we added this cool @VirtualProperty above the getProgrammerNickname() method:

... lines 1 - 23
class Battle
{
... lines 26 - 123
/**
* @Serializer\VirtualProperty()
* @Serializer\SerializedName("programmer")
*/
public function getProgrammerNickname()
{
return $this->programmer->getNickname();
}
... lines 132 - 140
}

Get rid of that entirely.

In BattleControllerTest, let's see if this is working. First dump the response. Copy the method name, and give this guy a try:

./vendor/bin/phpunit --filter testPOSTCreateBattle

Ah! It explodes!

Warning: call_user_func_array() expects parameter 1 to be a valid callback. Class Battle does not have a method getProgrammerNickname().

Whoops! I think I was too aggressive. Remember, at the top of Battle.php, we have an expression that references this method:

... lines 1 - 10
/**
... lines 12 - 14
* @Hateoas\Relation(
* "programmer",
* href=@Hateoas\Route(
... line 18
* parameters={"nickname"= "expr(object.getProgrammerNickname())"}
* ),
... line 21
* )
*/
class Battle
... lines 25 - 139

So... let's undo that change: put back getProgrammerNickname(), but remove the @VirtualProperty:

... lines 1 - 23
class Battle
{
... lines 26 - 123
public function getProgrammerNickname()
{
return $this->programmer->getNickname();
}
... lines 129 - 137
}

All right, try it again:

./vendor/bin/phpunit --filter testPOSTCreateBattle

It passes! And the response looks exactly how we want: no more _embedded key.

We're not HAL-JSON'ing Anymore

But guess what, guys! We're breaking the rules of Hal! And this means that we are not returning HAL responses anymore. And that's OK: I want you to feel the freedom to make this choice.

We are still returning a consistent format that I want my users to know about, it's just not HAL. To advertise this, change the Content-Type to application/vnd.codebattles+json:

... lines 1 - 19
abstract class BaseController extends Controller
{
... lines 22 - 117
protected function createApiResponse($data, $statusCode = 200)
{
... lines 120 - 121
return new Response($json, $statusCode, array(
'Content-Type' => 'application/vnd.codebattles+json'
));
}
... lines 126 - 185
}

This tells a client that this is still JSON, but it's some custom vendor format. If we want to make friends, we should add some extra documentation to our API that explains how to expect the links and embedded data to come back.

Copy that and go into ProgrammerControllerTest and update our assertEquals() that's checking for the content type property:

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTProgrammerWorks()
{
... lines 18 - 30
$this->assertEquals('application/vnd.codebattles+json', $response->getHeader('Content-Type')[0]);
... lines 32 - 36
}
... lines 38 - 289
}

Finally, copy the test method name and let's make sure everything is looking good:

./vendor/bin/phpunit --filter testPOSTProgrammerWorks

All green!

I really love this HATEOAS library because it's so easy to add links to your API. But it doesn't mean that you have to live with HAL JSON. You can use a different official format or invent your own.

Leave a comment!

2
Login or Register to join the conversation
Anthony R. Avatar
Anthony R. Avatar Anthony R. | posted 5 years ago

Awesome post @weaverryan

'Content-Type' => 'application/vnd.codebattles+json'

In the wonderful book "Build APIs you won't hate", I could see that some people also include the version of the api so that a client would ask:

'Accept' => 'application/vnd.codebattles.v3+json'

This solves the caching problem when using other headers for api versioning - I thought this might interest some people maybe who were wondering how to do versioning too! (Apparently Github does it like this)

Reply

Very good tip Anthony R.! We didn't talk about versioning, but even if you're not thinking about versioning your api, you could add a v1 at the beginning so that you have it in case you ever need it :).

Cheers!

1 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