Buy

Customizing (making less ugly) Embeddeds!

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:

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

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

139 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 21
* )
*/
class Battle
... lines 25 - 139

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

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

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

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

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

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

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