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".
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 ?
@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
@EL Bachir Nouni Great.. Thanks... :)
Muito bom (very good)
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.
Interesting idea, though this is too big a question to answer here.
Any suggestion plz.
Very helpful, many thanks !
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?
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.
Ty for your reply.
It's works ;)
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!
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.Hi Faizal, I don't understand your comment, did it not work as expected?
Hello!
Can you help me showing how add annotation to class via php ?
What are you trying to accomplish? You might also take a look at http://php-and-symfony.matt...
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?
Sorry, I don't understand what you mean. Maybe you should look for help in the Doctrine community.