Back in the subscriber, create a new variable called $visitor
and set it to
$event->getVisitor()
. The visitor is kind of in charge of the serialization
process. And since we know we're serializing to JSON, this will be an instance
of JsonSerializationVisitor
. Write an inline doc for that and add a use statement
up top. That will give us autocompletion:
... lines 1 - 6 | |
use JMS\Serializer\JsonSerializationVisitor; | |
class LinkSerializationSubscriber implements EventSubscriberInterface | |
{ | |
public function onPostSerialize(ObjectEvent $event) | |
{ | |
/** @var JsonSerializationVisitor $visitor */ | |
$visitor = $event->getVisitor(); | |
... line 15 | |
} | |
... lines 17 - 28 | |
} |
Oh, hey, look at this - that class has a method on it called addData()
. We can use
it to add whatever cool custom fields we want. Add that new uri
field, but just set
it to the classic FOO
value for now:
... lines 1 - 12 | |
/** @var JsonSerializationVisitor $visitor */ | |
$visitor = $event->getVisitor(); | |
$visitor->addData('uri', 'FOO'); | |
... lines 16 - 30 |
The last thing we need to do - which you can probably guess - is register this
as a service. In services.yml
, add the service - how about link_serialization_subscriber
.
Add the class and skip arguments
- we don't have any yet. But we do need a tag
so that the JMS Serializer knows about our class. Set the tag name to jms_serializer.event_subscriber
:
... lines 1 - 5 | |
services: | |
... lines 7 - 29 | |
link_serialization_subscriber: | |
class: AppBundle\Serializer\LinkSerializationSubscriber | |
tags: | |
- { name: jms_serializer.event_subscriber } |
Ok, try the test! Copy the method name, head to the terminal and run:
./bin/phpunit -c app --filter testGETProgrammer
and then paste in the name. This method name matches a few tests, so we'll see more than just our one test run. Yes, it fails... but in a good way!
FOO
does not match/api/programmers/UnitTester
.
Above, we do have the new, custom uri
field.
This means we're almost done. To generate the real URI, we need the router. Add
the __construct()
method with a RouterInterface
argument. I'll use the option
+enter
shortcut to create that property and set it:
... lines 1 - 7 | |
use Symfony\Component\Routing\RouterInterface; | |
... lines 9 - 10 | |
class LinkSerializationSubscriber implements EventSubscriberInterface | |
{ | |
private $router; | |
public function __construct(RouterInterface $router) | |
{ | |
$this->router = $router; | |
} | |
... lines 19 - 45 | |
} |
In onPostSerialize()
say $programmer = $event->getObject();
. Because of our configuration
below, we know this will only be called when the object is a Programmer
. Add
some inline documentation for the programmer and plug in its use statement:
... lines 1 - 8 | |
use AppBundle\Entity\Programmer; | |
class LinkSerializationSubscriber implements EventSubscriberInterface | |
{ | |
... lines 13 - 19 | |
public function onPostSerialize(ObjectEvent $event) | |
{ | |
/** @var JsonSerializationVisitor $visitor */ | |
$visitor = $event->getVisitor(); | |
/** @var Programmer $programmer */ | |
$programmer = $event->getObject(); | |
... lines 26 - 32 | |
} | |
... lines 34 - 45 | |
} |
Finally, for the data type $this->router->generate()
and pass it api_programmers_show
and an array containing nickname
set to $programmer->getNickname()
:
... lines 1 - 10 | |
class LinkSerializationSubscriber implements EventSubscriberInterface | |
{ | |
... lines 13 - 19 | |
public function onPostSerialize(ObjectEvent $event) | |
{ | |
... lines 22 - 26 | |
$visitor->addData( | |
'uri', | |
$this->router->generate('api_programmers_show', [ | |
'nickname' => $programmer->getNickname() | |
]) | |
); | |
} | |
... lines 34 - 45 | |
} |
Cool! Now, go back to services.yml
and add an arguments
key with just @router
:
... lines 1 - 5 | |
services: | |
... lines 7 - 29 | |
link_serialization_subscriber: | |
class: AppBundle\Serializer\LinkSerializationSubscriber | |
arguments: ['@router'] | |
tags: | |
- { name: jms_serializer.event_subscriber } |
Ok, moment of truth! Run the test!
./bin/phpunit -c app --filter testGETProgrammer
And... it's failing. Ah, the URL has ?nickname=UnitTester
. Woh woh. I bet that's
my fault. The name of the route in onPostSerialize()
should be api_programmers_show
:
... lines 1 - 10 | |
class LinkSerializationSubscriber implements EventSubscriberInterface | |
{ | |
... lines 13 - 19 | |
public function onPostSerialize(ObjectEvent $event) | |
{ | |
... lines 22 - 26 | |
$visitor->addData( | |
'uri', | |
$this->router->generate('api_programmers_show', [ | |
'nickname' => $programmer->getNickname() | |
]) | |
); | |
} | |
... lines 34 - 45 | |
} |
Re-run the test:
./bin/phpunit -c app --filter testGETProgrammer
It's still failing, but for a new reason. This time it doesn't like the app_test.php
at the beginning of the link URI. Where's that coming from?
The test class extends an ApiTestCase
: we made this in an earlier episode. This
app already has a test
environment and it configures a test database connection.
If we can force every URL through app_test.php
, it'll use that test database, and
we'll be really happy:
... lines 1 - 20 | |
class ApiTestCase extends KernelTestCase | |
{ | |
... lines 23 - 46 | |
public static function setUpBeforeClass() | |
{ | |
... lines 49 - 59 | |
// guaranteeing that /app_test.php is prefixed to all URLs | |
self::$staticClient->getEmitter() | |
->on('before', function(BeforeEvent $event) { | |
$path = $event->getRequest()->getPath(); | |
if (strpos($path, '/api') === 0) { | |
$event->getRequest()->setPath('/app_test.php'.$path); | |
} | |
}); | |
... lines 68 - 69 | |
} | |
... lines 71 - 295 | |
} |
We did a cool thing with Guzzle to accomplish this: automatically prefixing our requests
with app_test.php
. But because of that, when we generate URLs, they will also have
app_test.php
. That's a good thing in general, just not when we're comparing URLs in a test.
Copy that path and create a helper function at the bottom of ApiTestCase
called
protected function adjustUri()
. Make this return /app_test.php
plus the $uri
.
This method can help when comparing expected URI's:
... lines 1 - 20 | |
class ApiTestCase extends KernelTestCase | |
{ | |
... lines 23 - 282 | |
/** | |
* Call this when you want to compare URLs in a test | |
* | |
* (since the returned URL's will have /app_test.php in front) | |
* | |
* @param string $uri | |
* @return string | |
*/ | |
protected function adjustUri($uri) | |
{ | |
return '/app_test.php'.$uri; | |
} | |
} |
Now, in ProgrammerControllerTest
, just wrap the expected URI in $this->adjustUri()
:
... lines 1 - 5 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 8 - 35 | |
public function testGETProgrammer() | |
{ | |
... lines 38 - 51 | |
$this->asserter()->assertResponsePropertyEquals( | |
$response, | |
'uri', | |
$this->adjustUri('/api/programmers/UnitTester') | |
); | |
} | |
... lines 58 - 231 | |
} |
This isn't a particularly incredible solution, but now we can properly test things. Run the tests again...
./bin/phpunit -c app --filter testGETProgrammer
And... It's green! Awesome!
One last thing! I mentioned that there are two ways to add super-custom fields
like uri
. Using a serializer subscriber is the first. But sometimes, your API
representation will look much different than your entity. Imagine we had some
crazy endpoint that returned info about a Programmer mixed with details about their
last 3 battles, the last time they fought and the current weather in their hometown.
Can you imagine trying to do this? You'll need multiple @VirtualProperty
methods
and probably some craziness inside an event subscriber. It might work, but it'll
look ugly and be confusing.
In this case, there's a much better way: create a new class with the exact properties you need. Then, instantiate it, populate the object in your controller and serialize it. This class isn't an entity - it's just there to model your API response. I love this approach and recommend it as soon as you're doing more than just a few serialization customizations to a class.