Your Own Custom Annotations – More than Just Comments!

Share this article

Your Own Custom Annotations – More than Just Comments!

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!

Frequently Asked Questions on Custom Annotations in PHP

What are the benefits of using custom annotations in PHP?

Custom annotations in PHP provide a way to add metadata to your code. This metadata can be used to influence the behavior of your code, making it more flexible and adaptable. For example, you can use annotations to define routing information in a web application, specify validation rules for form inputs, or configure dependency injection. This can lead to cleaner, more maintainable code as configuration details are kept alongside the code they relate to, rather than in separate configuration files.

How do I create a custom annotation in PHP?

Creating a custom annotation in PHP involves defining a new class that represents the annotation. This class should have properties that correspond to the values you want to be able to set in the annotation. You can then use this class as an annotation in your code by adding it in a comment above the relevant code element, such as a class, method, or property.

Can I use custom annotations with Doctrine?

Yes, Doctrine supports custom annotations. You can define your own annotations and use them in your Doctrine entities. Doctrine’s annotation reader will parse these annotations and you can then use them to influence the behavior of your entities.

Are there any limitations to using custom annotations in PHP?

One limitation of using custom annotations in PHP is that they are not natively supported by the language. This means you need to use a library or tool that provides annotation support, such as Doctrine or PHP-DI. Additionally, because annotations are part of comments, they are not checked by the PHP interpreter, so errors in annotations can be harder to catch.

How can I parse custom annotations in PHP?

To parse custom annotations in PHP, you need to use a library that provides annotation support. These libraries provide an annotation reader that can parse the annotations from your code. You can then access the values of these annotations in your code.

Can I use custom annotations in PHP without a library?

While it’s technically possible to use custom annotations in PHP without a library, it’s not recommended. Parsing annotations from comments is a complex task that involves dealing with the intricacies of the PHP language and its syntax. Using a library that provides annotation support abstracts away these complexities and makes working with annotations much easier.

What are some examples of how custom annotations can be used in PHP?

Custom annotations in PHP can be used in a variety of ways. For example, you can use annotations to define routing information in a web application, specify validation rules for form inputs, or configure dependency injection. You can also use annotations to add documentation to your code, such as describing the purpose of a class or method, or specifying the types of a method’s parameters and return value.

How can I debug custom annotations in PHP?

Debugging custom annotations in PHP can be challenging because annotations are part of comments and are not checked by the PHP interpreter. However, most libraries that provide annotation support also provide tools for debugging annotations. These tools can help you identify errors in your annotations and understand how they are being parsed.

Can custom annotations affect the performance of my PHP application?

The impact of custom annotations on the performance of your PHP application is generally minimal. Parsing annotations can take some time, but most libraries that provide annotation support cache the results of the parsing process, so the annotations are only parsed once.

Are custom annotations in PHP a good practice?

Whether or not using custom annotations in PHP is a good practice depends on the specific use case. Annotations can be a powerful tool for adding metadata to your code and influencing its behavior. However, they can also make your code harder to understand if used excessively or inappropriately. As with any tool, it’s important to use annotations judiciously and in a way that enhances the readability and maintainability of your code.

Daniel SiposDaniel Sipos
View Author

Daniel Sipos is a Drupal developer who lives in Brussels, Belgium. He works professionally with Drupal but likes to use other PHP frameworks and technologies as well. He runs webomelette.com, a Drupal blog where he writes articles and tutorials about Drupal development, theming and site building.

annotationsBrunoScommentcommentsdocblockdoctrineOOPHPPHPsymfonytype annotations
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week