PHP
Article

Your Own Custom Annotations – More than Just Comments!

By Daniel Sipos

In this article, we are going to look at how we can create and use our own custom annotations in a Symfony 3 application. You know annotations right? They are the docblock metadata/configuration we see above classes, methods and properties. You’ve most likely seen them used to declare Controller routes (@Route()) and Doctrine ORM mappings (@ORM()), or even to control access to various classes and methods in packages like Rauth. But have you ever wondered how can you use them yourself? How you can define your own annotation and then use it to read information about a class or method without actually loading it?

Snippet of code with custom annotations

Before we do this, a small disclaimer is in order. Annotations have nothing to do with Symfony. They are a concept developed as part of the Doctrine project to solve the problem of mapping ORM information to class methods.

In this article we are going to build a small reusable bundle called WorkerBundle. Reusable yes, but still only for demo purposes so not really package-able. We’re going to develop a small concept that allows the definition of various Worker types which “operate” at various speeds and which can then be used by anyone in the application. The actual worker operations are outside the scope of this post, since we are focusing on setting up the system to manage them (and discover them via annotations).

To see where we’re going, you can check out this repository and follow the instructions covered there for setting up the bundle in your local Symfony app.

The Workers

The workers will implement an interface that requires one method: ::work(). Inside our new WorkerBundle, let’s create a Workers/ directory to keep things tidy and add the interface there:

<?php

namespace WorkerBundle\Workers;

interface WorkerInterface
{
    /**
     * Does the work
     *
     * @return NULL
     */
    public function work();
}

The Annotation

Each worker has to implement the above interface. That’s clear. But aside from that, we need them to also have an annotation above the class in order to find them and read some metadata about them.

Doctrine maps the docblock annotation to a class whose properties represent the keys inside the annotation itself. Let’s create our own and see this in practice.

Each WorkerInterface instance will have the following annotation in its docblock:

/**
 * @Worker(
 *     name = "The unique Worker name",
 *     speed = 10
 * )
 */

We’re going to keep things simple and have only two properties: the unique name (string) and the worker speed (integer). In order for this annotation to be recognized by Doctrine’s annotation library, we’ll have to create a matching class which, not unexpectedly, has some annotations of its own.

We’ll put this class in the Annotation folder our bundle namespace and call it simply Worker:

<?php

namespace WorkerBundle\Annotation;

use Doctrine\Common\Annotations\Annotation;

/**
 * @Annotation
 * @Target("CLASS")
 */
class Worker
{
    /**
     * @Required
     *
     * @var string
     */
    public $name;

    /**
     * @Required
     *
     * @var int
     */
    public $speed;

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return int
     */
    public function getSpeed()
    {
        return $this->speed;
    }
}

As you can see, we have the two properties with some simple getters. More importantly, though, we have two annotations at the top: @Annotation (which tells Doctrine that this class represents an annotation) and @Target("CLASS") which tells it that it’s supposed to be used above an entire class and not a method or property. And that’s it, WorkerInterface classes can now use this annotation by making sure the corresponding class is also imported with a use statement at the top of the file like so:

use WorkerBundle\Annotation\Worker;

The Manager

Next, we need a manager our application can use to get a list of all the available workers and to create them. In the same namespace as the WorkerInterface, we can have this simple manager class:

<?php

namespace WorkerBundle\Workers;

class WorkerManager
{
    /**
     * @var WorkerDiscovery
     */
    private $discovery;


    public function __construct(WorkerDiscovery $discovery)
    {
        $this->discovery = $discovery;
    }

    /**
     * Returns a list of available workers.
     *
     * @return array
     */
    public function getWorkers() {
        return $this->discovery->getWorkers();
    }

    /**
     * Returns one worker by name
     *
     * @param $name
     * @return array
     *
     * @throws \Exception
     */
    public function getWorker($name) {
        $workers = $this->discovery->getWorkers();
        if (isset($workers[$name])) {
            return $workers[$name];
        }

        throw new \Exception('Worker not found.');
    }

    /**
     * Creates a worker
     *
     * @param $name
     * @return WorkerInterface
     *
     * @throws \Exception
     */
    public function create($name) {
        $workers = $this->discovery->getWorkers();
        if (array_key_exists($name, $workers)) {
            $class = $workers[$name]['class'];
            if (!class_exists($class)) {
                throw new \Exception('Worker class does not exist.');
            }
            return new $class();
        }

        throw new \Exception('Worker does not exist.');
    }
}

The WorkerManager class does two things: retrieves worker definitions (::getWorker() and ::getWorkers()) and instantiates them (::create()). As a constructor argument it retrieves a WorkerDiscovery object which we will write in a minute. The rest is pretty easy to understand. The ::create() method expects that each worker definition is an array which has a class key to be used for instantiation. We keep things simple here but of course in a real world scenario a next step would be to represent this definition using a separate class and delegate the actual instantiation to a factory.

The Discovery

The crucial part of our annotation demo is actually part of the Discovery process. Why? Because we are using the Worker annotation to determine whether the respective class should be considered a Worker. In doing so, we are using the metadata before actually instantiating the object. So let’s see our WorkerDiscovery class:

<?php

namespace WorkerBundle\Workers;

use WorkerBundle\Annotation\Worker;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\HttpKernel\Config\FileLocator;

class WorkerDiscovery
{
    /**
     * @var string
     */
    private $namespace;

    /**
     * @var string
     */
    private $directory;

    /**
     * @var Reader
     */
    private $annotationReader;

    /**
     * The Kernel root directory
     * @var string
     */
    private $rootDir;

    /**
     * @var array
     */
    private $workers = [];


    /**
     * WorkerDiscovery constructor.
     *
     * @param $namespace
     *   The namespace of the workers
     * @param $directory
     *   The directory of the workers
     * @param $rootDir
     * @param Reader $annotationReader
     */
    public function __construct($namespace, $directory, $rootDir, Reader $annotationReader)
    {
        $this->namespace = $namespace;
        $this->annotationReader = $annotationReader;
        $this->directory = $directory;
        $this->rootDir = $rootDir;
    }

    /**
     * Returns all the workers
     */
    public function getWorkers() {
        if (!$this->workers) {
            $this->discoverWorkers();
        }

        return $this->workers;
    }

    /**
     * Discovers workers
     */
    private function discoverWorkers() {
        $path = $this->rootDir . '/../src/' . $this->directory;
        $finder = new Finder();
        $finder->files()->in($path);

        /** @var SplFileInfo $file */
        foreach ($finder as $file) {
            $class = $this->namespace . '\\' . $file->getBasename('.php');
            $annotation = $this->annotationReader->getClassAnnotation(new \ReflectionClass($class), 'WorkerBundle\Annotation\Worker');
            if (!$annotation) {
                continue;
            }

            /** @var Worker $annotation */
            $this->workers[$annotation->getName()] = [
                'class' => $class,
                'annotation' => $annotation,
            ];
        }
    }
}

The first two constructor arguments are part of configuring our bundle. They are both a string that tells our discovery which folder it should look into and which namespace to use for loading the found classes. These two are fed from the service container definition we will see at the end. The rootDir argument is the simple path to the Kernel directory and the Reader instance is the Doctrine class we use for reading annotations. All of the magic happens inside ::discoverWorkers().

First, we establish the path where to look for workers. Then, we use the Symfony Finder component to look up all the files in that folder. Iterating through all the found files, we establish the class names of all the found classes based on the file name and create ReflectionClass instances we then pass to the annotation reader’s ::getClassAnnotation(). The second argument to this method represents the namespace of the Annotation class to be used. And for all the annotations we find, we build an array of definitions containing the class name that can be used for instantiation and the entire annotation object if somebody needs it. That’s it! Our discovery service can now look up workers without instantiating any of them.

For more information, inspect the AnnotationReader class for the possibilities you have for extracting annotation data. There are also methods for reading method and property annotations which you can use by passing the relevant reflection objects.

Wiring It Up

Now that we have our main components, it’s time to wire everything up. First, we need our service definitions, so inside the Resource/config folder of our bundle we can have this services.yml file:

services:
    worker_manager:
        class: WorkerBundle\Workers\WorkerManager
        arguments: ["@worker_discovery"]
    worker_discovery:
        class: WorkerBundle\Workers\WorkerDiscovery
        arguments: ["%worker_namespace%", "%worker_directory%", "%kernel.root_dir%", "@annotation_reader"]

Nothing major is happening here. The WorkerManager gets the WorkerDiscovery as a dependency while the latter gets some parameters and the Doctrine annotation reader service.

But in order for our service definitions to be picked up centrally by the container, we need to write a small extension class. So inside the DependencyInjection folder of our bundle, create a class called WorkerExtension. Both the location and name are important for Symfony to pick it up automatically.

<?php

namespace WorkerBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class WorkerExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yml');
    }
}

In here we are just using the ::load() method to include our services into the container copy that gets merged with the main container.

The last thing that we need to do is to register our bundle. Inside our AppKernel:

public function registerBundles()
{
    return array(
        // ...
        new WorkerBundle\WorkerBundle(),
        // ...
    );
}

And that’s it.

Let’s Work!

Now we are ready to work. Let’s configure where our workers will be found inside the central parameters.yml file:

worker_namespace: AppBundle\Workers
worker_directory: AppBundle/Workers

These parameters are passed from the container to the WorkerDiscovery class as we’ve seen above.

The location is up to you but for now let’s put our workers in the main AppBundle bundle of our application. Our first worker can look like this:

<?php

namespace AppBundle\Workers;

use WorkerBundle\Annotation\Worker;
use WorkerBundle\Workers\WorkerInterface;

/**
 * Class SlowWorker
 *
 * @Worker(
 *     name = "Slow Worker",
 *     speed = 5
 * )
 */
class SlowWorker implements WorkerInterface
{

    /**
     * {@inheritdoc}
     */
    public function work()
    {
        return 'I work really slowly';
    }
}

Our annotation is up there, the use statement is in place so there is nothing preventing some business logic from finding it and instantiating it. Let’s assume inside a Controller method:

$manager = $this->get('worker_manager');
$worker = $manager->create('Slow Worker');
$worker->work();

or

$workers = $manager->getWorkers();
$workers = array_filter($workers, function($definition) {
    return $definition['annotation']->getSpeed() >= 5;
});
foreach($workers as $definition) {
    /** @var WorkerInterface $worker */
    $worker = $manager->create($definition['annotation']->getName());
    $worker->work();
}

Pfff… our SlowWorker barely made it in to work today!

Conclusion

We now have the power of using annotations to express metadata about our classes (or methods and properties). In this tutorial, we’ve built a small package that exposes the possibility for the application (or other external bundles) to declare workers capable of performing some work by defining some metadata about them. This metadata not only makes them easily discoverable, but also provides information on whether or not they should actually be employed.

Do you use custom annotations in your own projects? If so, how do you implement them the Rauth way, or like we did here? Perhaps a third approach? Let us know!


Interested in learning more about Symfony, Doctrine, annotations, and all manner of enterprisey PHP things? Join us for a fully packed three days of hands-on workshops at WebSummerCamp – the only conference that’s exclusively hands-on, and also takes care of anyone you’d like to bring along!

  • Marek Králík

    Not this annotations crap again. Back in 2011 I knew this was stupid idea and recently I came to a mid-size project where custom annotations already becoming nightmare.

    • Giorgio Santini

      Annotations become nightmare if project manager and developers don’t know how to use them. The thruth is PHP and most 3rd libraries and frameworks lack in annotations support and don’t use them, leading to big fat classes, high coupling, repeated code. Take a look at some Java frameworks, i.e. spring, where everithing could be handled by annotations, saving time and useless configuration coding, lowing coupling, etc. Annotations are powerful, and PHP sould give native support to them instead of forcing developers to discover creative methods to read them.

    • http://www.bitfalls.com/ Bruno Skvorc

      Doctrine and Symfony, both more than mid-size projects, have been using them extensively for years with great success. What was your negative experience with them?

      • Marek Králík

        They are not using them. They are “demanding” you to use them. Don’t confuse yourself. Of course you won’t have a problem doing your sh*t when you created it, but just wait for the moment when you want to do something they wasn’t designed to do. If your code is pure PHP, you will have a lots of tools to handle that situation. If you use annotations you’ll end up rewriting code to PHP or worse you are left with some logic written in PHP and other half in annotations.

        • http://www.bitfalls.com/ Bruno Skvorc

          But you’re not being specific at all. You can’t just shout “annotations are bad” and feel better because you complained with a slogan most people are happy to regurgitate without backing it up. Get some specific examples of “annotation badness” out there and let’s discuss them, the pros, the cons, the whole nine yards. I’d like to fight the good fight with you, but I can’t if you’re being so unspecific and generalist. Let’s get into the details and discuss cases!

          P.S. Neither Symfony nor Doctrine demand annotations. They support them, but you can opt out.

        • http://ocramius.github.io/ Marco Pivetta

          They are “demanding” you to use them.

          Can you please point to a manual reference? Doctrine ORM supports XML, Yaml and native PHP mappings, and I personally endorse XML mappings for long-living projects. I don’t see an enforcement rule anywhere…

          • Asmir Mustafic

            Of course they are not mandatory, but if you use them, then you have to load handlers even when not necessary.

            As example, I have a web Api with doctrine, using annotations almost for everything, mainly doctrine and jms serializer annotations.
            Later I split the Api in two repo,
            1. Web api (containg controllers and Web stuff)
            2. Core (containing mainly entities and some business logic services)

            Till here everything went fine.

            Later I have created a “backoffice” app with its own set of controllers and Web stuff, I have included the Core repo via composer dependency .

            But in this step, the annotation loader started complaining since on the backoffice there is no jms serislizer.

            Solution: added jms on backoffice… is doing nothing, but is loaded anyway as dependency.

            After many years I found a compromise: annotations save so much time during development, that is totaly worth load some extra code….even if not required… of course in some case, my credo can not be applied

          • http://ocramius.github.io/ Marco Pivetta

            But in this step, the annotation loader started complaining since on the backoffice there is no jms serislizer.

            It would have complained even with XML or other mapping types, since you are still initializing some sort of metadata.

            Otherwise, you could set all annotations from the JMSSerializer namespace to be ignored, which would probably lead to worse hidden bugs.

  • http://www.ToonVerwerft.be/ Toon Verwerft

    Personally, I think that the example in this article is a bad use case of
    annotations. You should definetly use tagged services with a compiler
    pass for this kind of implementations in Symfony.

    I do like to use annotations in situations where they help me focus more on the business rules behind the code. For example: We use Cache annotations to make sure we don’t have to validate if a key is in the cache pool in every method or inject the cache manager into every class that wants to use this feature.
    This makes it easier to understand what is going on in the code, but gives us a configurable way of caching the results.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.