Buy Access to Course
08.

Adding Links via Annotations

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Oh man, this chapter will be one of my favorite ever to record, because we're going to do some sweet stuff with annotations.

In ProgrammerControllerTest, we called this key uri:

// ... 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
}

Because, well... why not?

But when we added pagination, we included its links inside a property called _links:

// ... lines 1 - 5
class ProgrammerControllerTest extends ApiTestCase
{
// ... lines 8 - 76
public function testGETProgrammersCollectionPaginated()
{
// ... lines 79 - 101
$this->asserter()->assertResponsePropertyExists($response, '_links.next');
// ... lines 103 - 125
}
// ... lines 127 - 231
}

That kept links separate from data. I think we should do the same thing with uri: change it to _links.self. The key self is a name used when linking to, your, "self":

// ... lines 1 - 5
class ProgrammerControllerTest extends ApiTestCase
{
// ... lines 8 - 35
public function testGETProgrammer()
{
// ... lines 38 - 51
$this->asserter()->assertResponsePropertyEquals(
$response,
'_links.self',
$this->adjustUri('/api/programmers/UnitTester')
);
}
// ... lines 58 - 231
}

Renaming this is easy, but we have a bigger problem. Adding links is too much work. Most importantly, the subscriber only works for Programmer objects - so we'll need more event listeners in the future for other classes.

I have a different idea. Imagine we could link via annotations, like this: add @Link with "self" inside, route = "api_programmers_show" params = { }. This route has a nickname wildcard, so add "nickname": and then use the expression object.getNickname():

196 lines | src/AppBundle/Entity/Programmer.php
// ... lines 1 - 9
/**
* Programmer
// ... lines 12 - 15
* @Link(
* "self",
* route = "api_programmers_show",
* params = { "nickname": "object.getNickname()" }
* )
*/
class Programmer
{
// ... lines 24 - 194
}

That last part is an expression, from Symfony's expression language. You and I are going to build the system that makes this work, so I'm going to assume that we'll pass a variable called object to the expression language that is this Programmer object being serialized. Then, we just call .getNickname().

Of course, this won't work yet - in fact it'll totally bomb if you try it. But it will in a few minutes!

Creating an Annotation

To create this cool system, we need to understand a bit about annotations. Every annotation - like Table or Entity from Doctrine - has a class behind it. That means we need a Link class. Create a new directory called Annotation. Inside add a new Link class in the AppBundle\Annotation namespace:

27 lines | src/AppBundle/Annotation/Link.php
<?php
namespace AppBundle\Annotation;
// ... lines 4 - 8
class Link
{
// ... lines 11 - 25
}

To hook this annotation into the annotations system, we need a few annotations: the first being, um, well, @Annotation. Yep, I'm being serious. The second is @Target, which will be "CLASS". This means that this annotation is expected to live above class declarations:

27 lines | src/AppBundle/Annotation/Link.php
// ... lines 1 - 4
/**
* @Annotation
* @Target("CLASS")
*/
class Link
{
// ... lines 11 - 25
}

Inside the Link class, we need to add a public property for each option that can be passed to the annotation, like route and params. Add public $name;, public $route; and public $params = array();:

27 lines | src/AppBundle/Annotation/Link.php
// ... lines 1 - 8
class Link
{
// ... lines 11 - 15
public $name;
// ... lines 17 - 22
public $route;
public $params = array();
}

The first property becomes the default property, which is why we don't need to have name = "self" when using it.

The name and route options are required, so add an extra @Required above them:

27 lines | src/AppBundle/Annotation/Link.php
// ... lines 1 - 8
class Link
{
/**
* @Required
*
* @var string
*/
public $name;
/**
* @Required
*
* @var string
*/
public $route;
// ... lines 24 - 25
}

And... that's it!

Inside of Programmer, every annotation - except for the special @Annotation and @Target guys, they're core to that system - needs a use statement - we already have some for @Serializer, @Assert and @ORM. Add a use statement directly to the class itself for @Link:

196 lines | src/AppBundle/Entity/Programmer.php
// ... lines 1 - 7
use AppBundle\Annotation\Link;
/**
* Programmer
*
* @ORM\Table(name="battle_programmer")
* @ORM\Entity(repositoryClass="AppBundle\Repository\ProgrammerRepository")
* @Serializer\ExclusionPolicy("all")
* @Link(
* "self",
* route = "api_programmers_show",
* params = { "nickname": "object.getNickname()" }
* )
*/
class Programmer
{
// ... lines 24 - 194
}

This hooks the annotation up with the class we just created.

Reading the Annotation

Ok... so how do we read annotations? Great question, I have no idea. Ah, it's easy, thanks to the Doctrine annotations library that comes standard with Symfony. In fact, we already have a service available called @annotation_reader.

Inside LinkSerializationSubscriber, inject that as the second argument. It's an instance of the Reader interface from Doctrine\Common\Annotations. Call it $annotationsReader:

// ... lines 1 - 5
use Doctrine\Common\Annotations\Reader;
// ... lines 7 - 12
class LinkSerializationSubscriber implements EventSubscriberInterface
{
private $router;
private $annotationReader;
// ... lines 18 - 20
public function __construct(RouterInterface $router, Reader $annotationReader)
{
$this->router = $router;
$this->annotationReader = $annotationReader;
// ... line 25
}
// ... lines 27 - 72
}

I'll hit option+enter and select initialize fields to get that set on property.

And before I forget, in services.yml, inject that by adding @annotation_reader as the second argument:

35 lines | app/config/services.yml
// ... lines 1 - 5
services:
// ... lines 7 - 29
link_serialization_subscriber:
class: AppBundle\Serializer\LinkSerializationSubscriber
arguments: ['@router', '@annotation_reader']
tags:
- { name: jms_serializer.event_subscriber }

Super easy.

Too easy, back to work! Delete all of this junk in onPostSerialize() and start with $object = $event->getObject(). To read the annotations off of that object, add $annotations = $this->annotationReader->getClassAnnotations(). Pass that a new \ReflectionObject() for $object:

// ... lines 1 - 12
class LinkSerializationSubscriber implements EventSubscriberInterface
{
// ... lines 15 - 27
public function onPostSerialize(ObjectEvent $event)
{
/** @var JsonSerializationVisitor $visitor */
$visitor = $event->getVisitor();
$object = $event->getObject();
$annotations = $this->annotationReader
->getClassAnnotations(new \ReflectionObject($object));
// ... lines 36 - 50
}
// ... lines 52 - 72
}

That's it!

Now, the class could have a lot of annotations above it, but we're only interested in the @Link annotation. We'll add an if statement to look for that in a second. But first, create $links = array(): that'll be our holder for any links we find:

// ... lines 1 - 32
$object = $event->getObject();
$annotations = $this->annotationReader
->getClassAnnotations(new \ReflectionObject($object));
$links = array();
// ... lines 38 - 74

Now, foreach ($annotations as $annotations). Immediately, see if this is something we care about with if ($annotation instanceof Link). At this point, the annotation options are populated on the public properties of the Link object. To get the URI, we can just say $this->router->generate() and pass it $annotation->route and $annotation->params:

// ... lines 1 - 36
$links = array();
foreach ($annotations as $annotation) {
if ($annotation instanceof Link) {
$uri = $this->router->generate(
$annotation->route,
$this->resolveParams($annotation->params, $object)
);
// ... line 44
}
}
// ... lines 47 - 74

How sweet is that? Well, we're not done yet: the params contain an expression string... which we're not parsing yet. We'll get back to that in a second.

Finish this off with $links[$annotation->name] = $uri;. At the bottom, finish with the familiar $visitor->addData() with _links set to $links;. Other than evaluating the expression, that's all the code you need:

// ... lines 1 - 12
class LinkSerializationSubscriber implements EventSubscriberInterface
{
// ... lines 15 - 27
public function onPostSerialize(ObjectEvent $event)
{
// ... lines 30 - 36
$links = array();
foreach ($annotations as $annotation) {
if ($annotation instanceof Link) {
$uri = $this->router->generate(
$annotation->route,
$this->resolveParams($annotation->params, $object)
);
$links[$annotation->name] = $uri;
}
}
// ... line 48
$visitor->addData('_links', $links);
// ... line 50
}
// ... lines 52 - 72
}

Check this out by going to /api/programmers in the browser. Look at that! The embedded programmer entities actually have a link called self. It worked!

Of course, the link is totally wrong because we're not evaluating the expression yet. But, we're really close.