rfc:annotations

This is an old revision of the document!


Request for Comments: Class Metadata

Introduction

Many languages currently support metadata information. This RFC exposes an idea about how can it be implemented in PHP, providing a powerful tool that many applications can take advantage of.

Why do we need Class Metadata?

Frameworks in general rely on metadata information in order to correctly work. They can use it for many purposes:

  • phpUnit Providing meta functionality for test cases, examples: @dataProvider for test data iteration, @expectedException for catching exceptions, etc.
  • Doctrine For Object-Relational mapping, examples: @Entity, @OneToOne, @Id, etc.
  • Zend Framework Server classes Used to automate mappings for XML-RPC, SOAP, etc.
  • FLOW3 for dependency injection and validation
  • Symfony2 for routing rules
  • Others One clear thing that comes to my mind is Validation, Functional Behavior injection (which could take advantage of Traits), etc. Also, any Framework could take advantage of it somehow.

So, any meta mapping injection could be easily achieved via the implementation of a centralized Annotations support.

The .NET framework uses Data Annotation: http://www.asp.net/mvc/tutorials/validation-with-the-data-annotation-validators-cs

An advantage here is the .net framework will process some annotations and inject behavior into the compiled source code.

It's important to note that annotations exist in java and .net but many strong use cases exist in these languages to provide hints to the compiler (@NotNull).

These types of use cases (hints to the Zend lexer/parser or other PHP implementations) are not presented in this RFC.

Common Misconceptions

Metadata mapping is commonly referred an feature that cannot be used widely, so its implementation is useless. As pointed previously, there're many use cases for this support.

Though useful, the good and bad use cases of annotations are heavily debated (religiously):

http://willcode4beer.com/design.jsp?set=annotations_gotchas_best_practices

http://www.softwarereality.com/programming/annotations.jsp

Proposal

First thing to be decided would be the tokens to be used for categorize an Annotation.

When using meta mapping, less characters is preferred to speed up its construction.

PHP Annotation could be simplified into this EBNF:

Annotations     ::= Annotation {Annotation}*
Annotation      ::= "[" AnnotationName ["(" [Values] ")"] "]"
AnnotationName  ::=  QualifiedName | SimpleName | AliasedName
QualifiedName   ::= {"\"}* NameSpacePart "\" {NameSpacePart "\"}* SimpleName
AliasedName     ::= Alias ":" SimpleName
NameSpacePart   ::= identifier
SimpleName      ::= identifier
Alias           ::= identifier
Values          ::= Array | Value {"," Value}*
Value           ::= PlainValue | FieldAssignment
PlainValue      ::= integer | string | float | boolean | Array | Annotation
FieldAssignment ::= FieldName "=" PlainValue
FieldName       ::= identifier
Array           ::= "array(" ArrayEntry {"," ArrayEntry}* ")"
ArrayEntry      ::= Value | KeyValuePair
KeyValuePair    ::= Key "=>" PlainValue
Key             ::= string | integer

With identifier being the terminal: [a-zA-Z_][a-zA-Z0-9_]* The annotation can be a composite of a start/end tokens. Here is an example of PHP Annotations:

[Entity(tableName="users")]
class User
{
    [Column(type="integer")]
    [Id]
    [GeneratedValue(strategy="AUTO")]
    protected $id;

    // ...

    [ManyToMany(targetEntity="Phonenumber")]
    [JoinTable(
        name="users_phonenumbers",
        joinColumns=array(
            [JoinColumn(name="user_id", referencedColumnName="id")]
        ),
        inverseJoinColumns=array(
            [JoinColumn(name="phonenumber_id", referencedColumnName="id", unique=true)]
        )
    )]
    protected $Phonenumbers;
}

The support is all done through the inclusion of a new class: ReflectionAnnotation.

How to define Annotations

Annotations can be defined on classes, methods, properties, functions or function parameters. ReflectionAnnotation is an abstract class the must be implemented in order to accept an Annotation definition. Once this class is extended, the subclass is ready to be used an an Annotation:

class Foo extends \ReflectionAnnotation {}
 
[Foo(true)]
class Bar { /* ... */ }

Just extending the base class already allows you to define a unique value, accessible through a public property named “value”.

$reflClass = new \ReflectionClass('Bar');
$reflAnnot = $reflClass->getAnnotation('Foo');
 
echo $foo->value; // true

To expand Annotations support, it is allowed to define other properties. BY doing it, you can define Annotations and automatically defining values to them.

namespace App\Annotation;
 
class Link extends \ReflectionAnnotation {
    public $url;
    public $target;
}
 
[App\Annotation\Link(url="http://www.php.net", target="_blank")]
class PHPWebsite {
    /* ... */
}

How to retrieve Annotation information

Annotations are only useful if defined information can be retrieved somehow. There are different ways to retrieve annotation information, mostly differing from how it was defined.

The following explanation is only valid for classes

To define an Annotation that may be exported to subclasses, it is required to define an Annotation on ReflectionAnnotation subclass, called Inherited. This is the general rule for Inherited annotations:

[Inherited] annotations are not inherited when used to annotate anything other than a class. A class that implements one or more interfaces never inherits any annotations from the interfaces it implements.

Example:

[Inherited]
class Foo extends \ReflectionAnnotation {}
 
class Bar extends \ReflectionAnnotation {}
 
[Foo]
[Bar]
class A {}
 
class B extends A {}

When you attempt to retrieve the defined information for classes A and B, you get:

$reflClassA = new \ReflectionClass('A');
var_dump($reflClassA->getAnnotations());
/*
array(2) {
  ["Foo"]=>
  object(Foo)#%d (1) {
    ["value"]=>NULL
  },
  ["Bar"]=>
  object(Bar)#%d (1) {
    ["value"]=>NULL
  }
}
*/
 
$reflClassB = new \ReflectionClass('B');
var_dump($reflClassB->getAnnotations());
/*
array(2) {
  ["Foo"]=>
  object(Foo)#%d (1) {
    ["value"]=>NULL
  }
}
*/

The method `getAnnotations()` supports one of the three arguments:

  • \ReflectionAnnotation::ALL - Fetched both defined annotations + inherited ones
  • \ReflectionAnnotation::INHERITED - Retrieves only inherited annotations
  • \ReflectionAnnotation::DEFINED - Retrieves only defined annotations (which may be defined as [Inherited], but declared in current class)

Another available method is to retrieve an specific Annotation: `getAnnotation($name)`, which may return the matched Annotation or null if not found.

At the level of a single code element (property, class, method...), you'll always get one single instance of a given annotation. This means that if you call getAnnotation multiple times on the same element, you'll always get the same instance.

Basically, these are the extended methods in Reflection API, written in raw PHP:

abstract class ReflectionAnnotation {
    const INHERITED = 1;
    const DECLARED  = 2;
    const ALL       = 3;
 
    public $value   = null;
 
    public function __construct(Reflector $reflector, array $properties = null) {
        if (is_array($properties)) {
            foreach ($properties as $k => $v) {
                $this->$k = $v;
            }
        }
    }
}
 
class ReflectionFunction {
    // ...
 
    public function getAnnotations();
    public function getAnnotation($name);
    public function hasAnnotation($name);
}
 
class ReflectionClass {
    // ...
 
    public function getAnnotations($type = ReflectionAnnotation::ALL);
    public function getAnnotation($name, $type = ReflectionAnnotation::ALL);
    public function hasAnnotation($name, $type = ReflectionAnnotation::ALL);
}
 
class ReflectionProperty {
    // ...
 
    public function getAnnotations();
    public function getAnnotation($name);
    public function hasAnnotation($name);
}
 
class ReflectionMethod {
    // ...
 
    public function getAnnotations();
    public function getAnnotation($name);
    public function hasAnnotation($name);
}
 
class ReflectionParameter {
    // ...
 
    public function getAnnotations();
    public function getAnnotation($name);
    public function hasAnnotation($name);
}

BC breaks

  • Creates two additional classes named “ReflectionAnnotation” and “Inherited” that may break existing code.
  • None otherwise (no new keywords)

Patch

Changelog

  • 2010-05-26 guilhermeblanco Initial RFC creation.
  • 2010-08-24 guilhermeblanco Updated for a real doable support
  • 2010-08-24 pierrick Add the patch
rfc/annotations.1302087566.txt.gz · Last modified: 2017/09/22 13:28 (external edit)