Symfony2 & Doctrine Common: creating powerful annotations

Posted on by Matthias Noback

I was looking into the Doctrine Common library; it seems to me that especially the AnnotationReader is quite interesting. Several Symfony2 bundles use annotation for quick configuration. For example adding an @Route annotation to your actions allows you to add them "automatically" to the route collection. The bundles that leverage the possibilities of annotation all use the Doctrine Common AnnotationReader (in fact, the cached version) for retrieving all the annotations from your classes and methods. The annotation reader works like this: it looks for annotations, for which it tries to instantiate a class. This may be a class made available by adding a use statement to your file. That is why you have to add use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route to any PHP file in which you use the @Route annotation.

So, what if you want to make your own annotations? It's quite simple actually. There are only a few catches. I will show you an example of a data converter which converts domain objects to standard PHP objects. I use annotations to configure the names of properties and the type of data that should be stored in these properties.

First create a class that serves as the annotation class (like Route):

namespace Acme\DataBundle\Annotation;

/**
 * @Annotation
 */
class StandardObject
{
    private $propertyName;
    private $dataType = 'string';

    public function __construct($options)
    {
        if (isset($options['value'])) {
            $options['propertyName'] = $options['value'];
            unset($options['value']);
        }

        foreach ($options as $key => $value) {
            if (!property_exists($this, $key)) {
                throw new \InvalidArgumentException(sprintf('Property "%s" does not exist', $key));
            }

            $this->$key = $value;
        }
    }

    public function getPropertyName()
    {
        return $this->propertyName;
    }

    public function getDataType()
    {
        return $this->dataType;
    }
}

The annotation class should have the annotation @Annotation in it's DocComment block. This specific class allows you to use annotations like this:

namespace Acme\DataBundle\Entity;

class Person
{
    /**
     * @StandardObject("name", dataType="string")
     */
    public function getName()
    {
        return 'Matthias Noback';
    }
}

The options "name" and dataType="string" will be given as an array to the constructor of the StandardObject annotation class. The array will contain the key "value" with the value "name" and the key "dataType" with the value "string".

Now let's see how we can use the new annotation. For example in the actual StandardObjectConverter that can be used to convert a domain object to a standard object.

As said before, converting annotations to annotation classes happens in the Doctrine Common's AnnotationReader. So we need an instance of this reader. You may for example use the service annotation_reader, which is by default available in Symfony2's service container. Or you can instantiate your own AnnotationReader (but than you won't have the benefits of caching, which is enabled by default when you use the service).

namespace Acme\DataBundle\Conversion;

use Doctrine\Common\Annotations\Reader;

class StandardObjectConverter
{
    private $reader;
    private $annotationClass = 'Acme\\DataBundle\\Annotation\\StandardObject';

    public function __construct(Reader $reader)
    {
        $this->reader = $reader;
    }

    public function convert($originalObject)
    {
        $convertedObject = new \stdClass;

        $reflectionObject = new \ReflectionObject($originalObject);

        foreach ($reflectionObject->getMethods() as $reflectionMethod) {
            // fetch the @StandardObject annotation from the annotation reader
            $annotation = $this->reader->getMethodAnnotation($reflectionMethod, $this->annotationClass);
            if (null !== $annotation) {
                $propertyName = $annotation->getPropertyName();

                // retrieve the value for the property, by making a call to the method
                $value = $reflectionMethod->invoke($originalObject);

                // try to convert the value to the requested type
                $type = $annotation->getDataType();
                if (false === settype($value, $type)) {
                    throw new \RuntimeException(sprintf('Could not convert value to type "%s"', $value));
                }

                $convertedObject->$propertyName = $value;
            }
        }

        return $convertedObject;
    }
}

Now you can do something like:

use Doctrine\Common\Annotations\AnnotationReader;
use Acme\DataBundle\Conversion\StandardObjectConverter;
use Acme\DataBundle\Entity\Person;

$reader = new AnnotationReader();
$converter = new StandardObjectConverter($reader);

$person = new Person();
$standardObject = $converter->convert($person);

This will result in a standard PHP object which has one property called name, whose value is "Matthias Noback".

PHP annotations Doctrine Common