Custom annotations in Symfony2 using Doctrine2's annotation classes

I'm sure you've already used annotations either in Symfony2 or another language (such as Python) that provides native support. PHP has lacked support for annotations for too long, thanks to Doctrine2 you can now write custom annotations without too much trouble.

Setting up

In order to create annotations you will need Doctrine2's annotations package, this should be included by default with Symfony2, if not then simply add the doctrine annotations package to composer:

Creating an annotation class

Once you've got the package you can start creating annotations. First off you will need to create an annotation class that will hold the annotations properties, this can be thought of as a meta class that will get attached to the target class you annotate. You will be able to use reflection methods to pull out the annotation and its settings for a particular class when you actually need to read the value of the annotation.

An annotations class is simply a parameter holder for the settings specified in each instance of the annotation, eg:

<?php
namespace MyProject\Annotations;

use Doctrine\Common\Annotations\Annotation;

/**
* @Annotation
* @Target("CLASS")
*/
final class ApiMeta extends Annotation
{
	public $resource;

	public $resourcePlural;
}

The code above is an example of a class annotation, specifically a Doctrine2 entity style annotation. As aluded to earlier you can create annotations for classes, functions and properties. The only difference between these annotation types is the value of 'Target' in the annotation declaration (here are the allowed values for annotation targets) and how you retrieve the annotation data, which I will get into shortly.

The annotation class needs to be annotated with the Annotation annotation in order to be usable. Extending the Annotation class is optional, it's constructor simply takes all the options specified in the annotation definition and adds them as properties to the annotation instance. So in the example below you will be able to use $annotationInstance->resource to retreive the value of resource for a particular object.

To use this annotation you would import it and assign values to the properties like so:

<?php

namespace MyProject\Entity;

use MyProject\Annotations\ApiMeta;

/**
* @ApiMeta(resource="myentity", resourcePlural="myentities")
*/
class MyEntity
{
    // Rest of class

Reading your annotation class

Ok so now you have an annotation and you've applied it to a class, let's see how to actually read the annotation information so you can do something with the annotation settings. I am just going to give an example of a class annotation here, but I will provide examples of property and function annotations at the end of the article.

To read an annotations settings you need to load Doctrine2's Annotations Reader class, using the reader you can use reflection objects to pull out any annotations that have been assigned to a class, property or function.

In the example below our client gets passed a namespace of an entity to load, it then passes an instance of PHP's ReflectionClass class to the Annotation Reader's 'getClassAnnotation' function, I specify the specific annotation that I want to retrieve as the second argument to this function. You can also use the 'getClassAnnotations' function with just a ReflectionClass instance to pull out all annotations associated with a class.

Here's the example code for using reading Annotations:

<?php

namespace MyProject\ApiBundle\Client;

use Doctrine\Common\Annotations\AnnotationReader;

class ApiClient
{
	public function get($entityClass)
	{
		$reader = new AnnotationReader();
		$apiMetaAnnotation = $reader->getClassAnnotation(new \ReflectionClass(new $entityClass), 'MyProject\\ApiBundle\\Annotations\\ApiMeta');
		if(!$apiMetaAnnotation) {
			throw new Exception(sprintf('Entity class %s does not have required annotation ApiMeta', $entityClass));
		}
		$resource = strtolower($apiMetaAnnotation->resource);
		$resourcePlural = strtolower($apiMetaAnnotation->resourcePlural);
		// do http lookup based on resource names
	}
}

Once an annotation instance has been retrieved you can access it's properties as you would a normal class, in this instance the client will use the apiMeta settings to query an API over HTTP.

Usage examples

You can read more on Doctrine2's annotations in the official documentation. As mentioned previously you can annotate classes, properties and methods, here are some examples of these.

Property annotation

Annotation class:

<?php

namespace MyProject\Annotations;

use Doctrine\Common\Annotations\Annotation;

/**
* @Annotation
* @Target({"PROPERTY"})
*/
final class ApiRelation extends Annotation
{
	public $type;

	public $class;

	public function single()
	{
		return $this->type == 'single' ? true : false;
	}

	public function multiple()
	{
		return $this->type == 'multiple' ? true : false;
	}
}

Using the annotation on a property:

<?php
namespace MyProject\Entity;

use MyProject\Annotations\ApiRelation;

class MyEntity
{
    /**
    * @ApiRelation("Section", type="single", class="MyProject\Entity\Section")
    */
    private $section;
    
    // Rest of class

Reading the annotation:

<?php

namespace MyProject\ApiBundle\Hydrator;

use Doctrine\Common\Annotations\AnnotationReader;

class Hydrator
{
	public static function hydrateObject($entityClass, $data)
	{
		$reader = new AnnotationReader();
		$reflectionObj = new \ReflectionObject(new $entityClass);
		
		foreach($data as $key => $value) {
			$property = Inflector::camelize($key);
			if($reflectionObj->hasProperty($property)) {
				$reflectionProp = new \ReflectionProperty($entityClass, $property);
				$relation = $reader->getPropertyAnnotation($reflectionProp, 'MyProject\\Annotations\\ApiRelation');

				if($relation) {
					if($relation->single()) {
						// Do something
					} else if($relation->multiple()) {
						// Do something
					}
				}
			}
		}
		// Do something
	}
}

Method annotation

Coming soon...