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

I have removed Disqus from this website, so for now, you can't comment on articles. In the near future you will be able to send comments by email. For now, if you want to say something, you can always send an email to me personally: info@matthiasnoback.nl.

Comments
John

Hello!
Can you help me showing how add annotation to class via php ?

Matthias Noback

What are you trying to accomplish? You might also take a look at http://php-and-symfony.matt...

John

Thanks!

I want to create Entity dinamically, so i know how create if from XML mapping, but i need add field wich will not db-field eg like "local" for Gedmo\Translatable extension, so.. what best way to add this field in Entity php class?

Matthias Noback

Sorry, I don't understand what you mean. Maybe you should look for help in the Doctrine community.

Faizal Pribadi

Not Show String Argument !

When var_dump($standardObject) // output stdclass

Matthias Noback

Hi Faizal, I don't understand your comment, did it not work as expected?

Reveclaire

Can we use the same method for creating annotation into the controllers ? As the SECURE annotation for example ? See my post on StackOverFlow here. And if not, how could it be possible ?
Thanks for your answer!

Matthias Noback

Hi, this is possible, and I've done it many times. Let me point you to another article of mine ;) http://php-and-symfony.matt... This will allow you to use annotations to prevent someone to execute a certain action.
The @Secure annotation is a bit more advanced though, since it will proxy your controller instead of just finding out at runtime which rules apply to the current controller. This is much faster, but I have not discovered a significant slowdown.

Leiha

Hi ^

I did a custom annotation for an entity.
It was enough easy.

When i look in cache file i see my annotation in the serialized object.
(My data are present without do another thing than create a class for custom annotation)

But after i don't understand who revover my data.
Reflection is useless i think because the serialized object is already filled.
Have you an idea , pls?

Matthias Noback

You don't need to recover anything from the cache yourself. It is the annotation reader itself that has a cache implementation, so you just call the usual methods on it. But instead of using reflection, it will reload the serialized annotations from the cache directory.

Leiha

Ty for your reply.
It's works ;)

Renrhaf

Very helpful, many thanks !

Dilip

Hi,

I am new in Symfony 2 i want text read from a PDF and show it's in particular page then select some contents for Annotation(like add title, description, Alias). How can do it in Symfony. Please help me.

Matthias Noback

Interesting idea, though this is too big a question to answer here.

Dilip

Any suggestion plz.

João Paulo Constantino

Muito bom (very good)

piter lelaona

Hii Matthias,

When i try to use this outside of symfony2, i have an error like this

Fatal error: Uncaught exception 'Doctrine\Common\Annotations\AnnotationException' with message '[Semantical Error] The annotation "@Pieter\Rbac\Annotation\StandardObject" in method Pieter\Rbac\Test\Person::getName() does not exist, or could not be auto-loaded.' in /Users/bro/Sites/annotation/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationException.php on line 54

How to fix this ?

EL Bachir Nouni

@piter lelaona : The solution to your probleme is :

- In your entry script you should add your autoloader to the AnnotationRegistry using the function : AnnotationRegistry::registerLoader.

Exemple : for my case i use composer, so in my entry script (index.php) i have the follow :

use Doctrine\Common\Annotations\AnnotationRegistry;
$loader = require_once './vendor/autoload.php';
AnnotationRegistry::registerLoader(array($loader, "loadClass"));
//the reste of code here

- also you should import your annotation before using it. the Person class should look like :

use Com\Annotations\StandardObject;
class Person
{

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

}
I hope this will help you :)

@Matthias : i think its better to add this to the post. thks for the post

piter lelaona

@EL Bachir Nouni Great.. Thanks... :)