The Doctrine Common annotations library was born from a need in the Doctrine2 ORM to allow the mapping information to be specified as metadata embedded in the class files, on properties and methods. The library is independent and can be used in your own libraries to implement doc block annotations.
There are several different approaches to handling annotations in PHP. Doctrine Annotations maps docblock annotations to PHP classes. Because not all docblock annotations are used for metadata purposes a filter is applied to ignore or skip classes that are not Doctrine annotations.
Take a look at the following code snippet:
<?php namespace MyProject\Entities; use Doctrine\ORM\Mapping AS ORM; use Symfony\Component\Validation\Constraints AS Assert; /** * @author Benjamin Eberlei * @ORM\Entity * @MyProject\Annotations\Foobarable */ class User { /** * @ORM\Id @ORM\Column @ORM\GeneratedValue * @dummy * @var int */ private $id; /** * @ORM\Column(type="string") * @Assert\NotEmpty * @Assert\Email * @var string */ private $email; }
In this snippet you can see a variety of different docblock annotations:
How are these annotations loaded? From looking at the code you could guess that the ORM Mapping, Assert Validation and the fully qualified annotation can just be loaded using the defined PHP autoloaders. This is not the case however: For error handling reasons every check for class existence inside the AnnotationReader sets the second parameter $autoload of class_exists($name, $autoload) to false. To work flawlessly the AnnotationReader requires silent autoloaders which many autoloaders are not. Silent autoloading is NOT part of the PSR-0 specification for autoloading.
This is why Doctrine Annotations uses its own autoloading mechanism through a global registry. If you are wondering about the annotation registry being global, there is no other way to solve the architectural problems of autoloading annotation classes in a straightforward fashion. Additionally if you think about PHP autoloading then you recognize it is a global as well.
To anticipate the configuration section, making the above PHP class work with Doctrine Annotations requires this setup:
<?php use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationRegistry; AnnotationRegistry::registerFile("/path/to/doctrine/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php"); AnnotationRegistry::registerAutoloadNamespace("Symfony\Component\Validator\Constraint", "/path/to/symfony/src"); AnnotationRegistry::registerAutoloadNamespace("MyProject\Annotations", "/path/to/myproject/src"); $reader = new AnnotationReader(); AnnotationReader::addGlobalIgnoredName('dummy');
The second block with the annotation registry calls registers all the three different annotation namespaces that are used. Doctrine saves all its annotations in a single file, that is why AnnotationRegistry#registerFile is used in contrast to AnnotationRegistry#registerAutoloadNamespace which creates a PSR-0 compatible loading mechanism for class to file names.
In the third block, we create the actual AnnotationReader instance. Note that we also add “dummy” to the global list of annotations for which we do not throw exceptions. Setting this is necessary in our example case, otherwise @dummy would trigger an exception to be thrown during the parsing of the docblock ofMyProject\Entities\User#id.
To use the annotations library is simple, you just need to create a new AnnotationReader instance:
<?php $reader = new \Doctrine\Common\Annotations\AnnotationReader();
This creates a simple annotation reader with no caching other than in memory (in php arrays). Since parsing docblocks can be expensive you should cache this process by using a caching reader.
You can use a file caching reader:
<?php use Doctrine\Common\Annotations\FileCacheReader; use Doctrine\Common\Annotations\AnnotationReader; $reader = new FileCacheReader( new AnnotationReader(), "/path/to/cache", $debug = true );
If you set the debug flag to true the cache reader will check for changes in the original files, which is very important during development. If you don’t set it to true you have to delete the directory to clear the cache. This gives faster performance, however should only be used in production, because of its inconvenience during development.
You can also use one of the Doctrine\Common\Cache\Cache cache implementations to cache the annotations:
<?php use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Cache\ApcCache; $reader = new CachedReader( new AnnotationReader(), new ApcCache(), $debug = true );
The debug flag is used here as well to invalidate the cache files when the PHP class with annotations changed and should be used during development.
The AnnotationReader works and caches under the assumption that all annotations of a doc-block are processed at once. That means that annotation classes that do not exist and aren’t loaded and cannot be autoloaded (using the AnnotationRegistry) would never be visible and not accessible if a cache is used unless the cache is cleared and the annotations requested again, this time with all annotations defined.
By default the annotation reader returns a list of annotations with numeric indexes. If you want your annotations to be indexed by their class name you can wrap the reader in an IndexedReader:
<?php use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\IndexedReader; $reader = new IndexedReader(new AnnotationReader());
You should never wrap the indexed reader inside a cached reader only the other way around. This way you can re-use the cache with indexed or numeric keys, otherwise your code may experience failures due to caching in an numerical or indexed format.
As explained in the Introduction Doctrine Annotations uses its own autoloading mechanism to determine if a given annotation has a corresponding PHP class that can be autoloaded. For Annotation Autoloading you have to configure the Doctrine\Common\Annotations\AnnotationRegistry. There are three different mechanisms to configure annotation autoloading:
Loaders have to fail silently, if a class is not found even if it matches for example the namespace prefix of that loader. Never is a loader to throw a warning or exception if the loading failed otherwise parsing doc block annotations will become a huge pain.
A sample loader callback could look like:
<?php use Doctrine\Common\Annotations\AnnotationRegistry; use Symfony\Component\ClassLoader\UniversalClassLoader; AnnotationRegistry::registerLoader(function($class) { $file = str_replace("\\", DIRECTORY_SEPARATOR, $class) . ".php"; if (file_exists("/my/base/path/" . $file)) { // file exists makes sure that the loader fails silently require "/my/base/path/" . $file; } }); $loader = new UniversalClassLoader(); AnnotationRegistry::registerLoader(array($loader, "loadClass"));
If you don’t want to specify the fully qualified class name or import classes with the use statement you can set the default annotation namespace using the setDefaultAnnotationNamespace() method. The following is an example where we specify the fully qualified class name for the annotation:
<?php /** @MyCompany\Annotations\Foo */ class Test { }
To shorten the above code you can configure the default namespace to be MyCompany\Annotations:
<?php $reader->setDefaultAnnotationNamespace('MyCompany\Annotations\\');
Now it can look something like:
<?php /** @Foo */ class Test { }
A little nicer looking!
You should only use this feature if you work in an isolated context where you have full control over all available annotations.
By default an exception is thrown from the AnnotationReader if an annotation was found that:
You can disable this behavior for specific names if your docblocks do not follow strict requirements:
<?php $reader = new \Doctrine\Common\Annotations\AnnotationReader(); AnnotationReader::addGlobalIgnoredName('foo');
By default the annotation reader parses the use-statement of a php file to gain access to the import rules and register them for the annotation processing. Only if you are using PHP Imports you can validate the correct usage of annotations and throw exceptions if you misspelled an annotation. This mechanism is enabled by default.
To ease the upgrade path, we still allow you to disable this mechanism. Note however that we will remove this in future versions:
<?php $reader = new \Doctrine\Common\Annotations\AnnotationReader(); $reader->setEnabledPhpImports(false);
If you want to define your own annotations you just have to group them in a namespace and register this namespace in the AnnotationRegistry. Annotation classes have to contain a class-level docblock with the text @Annotation:
<?php namespace MyCompany\Annotations; /** @Annotation */ class Bar { //some code }
The annotation parser check if the annotation constructor has arguments, if so then we will pass the value array, otherwise will try to inject values into public properties directly:
<?php namespace MyCompany\Annotations; /** * @Annotation * * Some Annotation using a constructor */ class Bar { private $foo; public function __construct(array $values) { $this->foo = $values['foo']; } } /** * @Annotation * * Some Annotation without a constructor */ class Foo { public $bar; }
@Target indicates the kinds of class element to which an annotation type is applicable. Then you could define one or more targets :
If the annotations is not allowed in the current context you got an AnnotationException
<?php namespace MyCompany\Annotations; /** * @Annotation * @Target({"METHOD","PROPERTY"}) */ class Bar { //some code } /** * @Annotation * @Target("CLASS") */ class Foo { //some code }
Annotation parser check the given parameters using the phpdoc annotation @var , The data type could be validated using the @var annotation on the annotation properties or using the annotations @Attributes and@Attribute.
If the data type not match you got an AnnotationException
<?php namespace MyCompany\Annotations; /** * @Annotation * @Target({"METHOD","PROPERTY"}) */ class Bar { /** @var mixed */ public $mixed; /** @var boolean */ public $boolean; /** @var bool */ public $bool; /** @var float */ public $float; /** @var string */ public $string; /** @var integer */ public $integer; /** @var array */ public $array; /** @var SomeAnnotationClass */ public $annotation; /** @var array<integer> */ public $arrayOfIntegers; /** @var array<SomeAnnotationClass> */ public $arrayOfAnnotations; } /** * @Annotation * @Target({"METHOD","PROPERTY"}) * @Attributes({ * @Attribute("stringProperty", type = "string"), * @Attribute("annotProperty", type = "SomeAnnotationClass"), * }) */ class Foo { public function __construct(array $values) { $this->stringProperty = $values['stringProperty']; $this->annotProperty = $values['annotProperty']; } // some code }
The use of constants and class constants are available on the annotations parser.
The following usage are allowed :
<?php namespace MyCompany\Entity; use MyCompany\Annotations\Foo; use MyCompany\Annotations\Bar; use MyCompany\Entity\SomeClass; /** * @Foo(PHP_EOL) * @Bar(Bar::FOO) * @Foo({SomeClass::FOO, SomeClass::BAR}) * @Bar({SomeClass::FOO_KEY = SomeClass::BAR_VALUE}) */ class User { }
Be careful with constants and the cache !
The cached reader will not re-evaluate each time an annotation is loaded from cache. When a constant is changed the cache must be cleaned.
Using the library API is simple. Using the annotations described in the previous section you can now annotate other classes with your annotations:
<?php namespace MyCompany\Entity; use MyCompany\Annotations\Foo; use MyCompany\Annotations\Bar; /** * @Foo(bar="foo") * @Bar(foo="bar") */ class User { }
Now we can write a script to get the annotations above:
<?php $reflClass = new ReflectionClass('MyCompany\Entity\User'); $classAnnotations = $reader->getClassAnnotations($reflClass); foreach ($classAnnotations AS $annot) { if ($annot instanceof \MyCompany\Annotations\Foo) { echo $annot->bar; // prints "foo"; } else if ($annot instanceof \MyCompany\Annotations\Bar) { echo $annot->foo; // prints "bar"; } }
You have a complete API for retrieving annotation class instances from a class, property or method docblock:
Some IDEs already provide support for annotations: