If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLet 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. ClassBattle
does not have a methodgetProgrammerNickname()
.
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.
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.
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!
// 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
}
}
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)