rfc:attributes

This is an old revision of the document!


PHP RFC: Attributes

Introduction

Attributes (or annotations) are a form of syntactic metadata that can be added to language classes, functions, etc. PHP offers only a single form of such metadata - doc-comments. This is just a string and to keep some structured information, we had to use some pseudo-language. Then we have to parse it to access a particular element of that structure.

Many languages like Java, C#, Hack, etc provide a simpler way. They allow the definition of structured meta-information through small syntax extension.

Proposal

Attribute Syntax

An attribute is a specially formatted text enclosed with "<<" and ">>". Attributes may be applied to functions, classes, interfaces, traits, methods, properties and class constants. In the same way as doc-comments, attributes must be placed before the corresponding definition, but it's possible to define more than one attribute one the same declaration.

<<...>>
<<...>>
function foo() {}

Each attribute definition construct may also define one or more named attribute, which may be used with no value, a single value or multiple values. Cf. the EBNF:

<attribute> ::= "<<" <namespace-name> [ "(" <value> { "," <value> } ")" ]
                { "," <namespace-name> [ "(" <value> { "," <value> } ")" ] } ">>".
<name>      ::= STRING.
<value>     ::= <php-constant> | <php-expression>.

And Example:

<<WithoutValue, SingleValue(0), FewValues('Hello', 'World')>>
function foo() {}

It's not possible to use the same attribute name more than once on the same definition, however, it's possible to use multiple attribute values associated with this name.

<<test(1),test(2)>> // Error
function foo() {}
 
<<test(1,2)>> // Works
function foo() {}

Arbitrary PHP Expressions as Attribute Values (AST attributes)

Other than simple scalars, attribute values may be represented with any valid PHP expression.

<<test($a + $b > 0)>>
function foo($a, $b) {
}

In this case, internally, the value of the attribute is kept as an Abstract Syntax Tree, and the user will have the ability to read every individual node of this tree separately. This approach implies the usage of the same PHP syntax for meta data and eliminates the need for a separate parser.

The native usage of an AST is not especially necessary. It's also possible to use plain strings and then transform them into AST at user level, through the php-ast extension.

<<test("$a + $b > 0")>>
function foo($a, $b) {
}
$r = new ReflectionFunction("foo");
$ast = ast\parse_code($r->getAttributes()["test"][0]);

Reflection

Reflection classes are extended with the getAttributes() method, and returns array of attributes.

function ReflectionFunction::getAttributes(): array;
function ReflectionClass::getAttributes(): array;
function ReflectionProperty::getAttributes(): array;
function ReflectionClassConstant::getAttributes(): array;

These functions return empty array if there were no attributes defined. Otherwise, they return an array with attribute names as keys and nested arrays as the corresponding values. Attributes without values represented by empty arrays, attributes with single value by arrays with a single element, etc.

<<WithoutValue, SingleValue(0), FewValues('Hello', 'World')>>
function foo() {}
$r = new ReflectionFunction("foo");
var_dump($r->getAttributes());
array(3) {
  ["WithoutValue"]=>
  array(0) {
  }
  ["SingleValue"]=>
  array(1) {
    [0]=>
    int(0)
  }
  ["FewValues"]=>
  array(2) {
    [0]=>
    string(5) "Hello"
    [1]=>
    string(5) "World"
  }
}

AST Representation

While internally AST is stored in native zend_ast format, Reflection*::getAttributes() methods return the corresponding representation built with objects of \ast\Node and \ast\Node\Decl classes, borrowed from php-ast. These classes moved onto PHP core may be used even without php-ast extension. However, it also defines useful constants and functions, that would simplify work with AST in PHP.

<<test($a + $b > 0)>>
function foo($a, $b) {
}
$r = new ReflectionFunction("foo");
var_dump($r->getAttributes());
array(1) {
  ["test"]=>
  array(1) {
    [0]=>
    object(ast\Node)#2 (4) {
      ["kind"]=>
      int(521)
      ["flags"]=>
      int(0)
      ["lineno"]=>
      int(0)
      ["children"]=>
      array(2) {
        [0]=>
        object(ast\Node)#3 (4) {
          ["kind"]=>
          int(520)
          ["flags"]=>
          int(1)
          ["lineno"]=>
          int(0)
          ["children"]=>
          array(2) {
            [0]=>
            object(ast\Node)#4 (4) {
              ["kind"]=>
              int(256)
              ["flags"]=>
              int(0)
              ["lineno"]=>
              int(0)
              ["children"]=>
              array(1) {
                [0]=>
                string(1) "a"
              }
            }
            [1]=>
            object(ast\Node)#5 (4) {
              ["kind"]=>
              int(256)
              ["flags"]=>
              int(0)
              ["lineno"]=>
              int(0)
              ["children"]=>
              array(1) {
                [0]=>
                string(1) "b"
              }
            }
          }
        }
        [1]=>
        int(0)
      }
    }
  }
}

Use Cases

With attributes it's extremely simple to mark some functions with some specific attribute and then perform checks and special handling in extensions.

<<inline>>
function add(int $a, $int $b): int {
  return $a + $b;
}
 
<<jit>>
function foo() {
  ...
}

Attributes may be used to provide an annotation system similar to Doctrine, where each attribute is represented by an object of corresponding class that perform validation and other actions.

<?php
namespace Doctrine\ORM {
 
	class Entity {
		private $name;
		public function __construct($name) {
			$this->name = $name;
		}
	}
 
	function GetClassAttributes($class_name) {
		$reflClass = new \ReflectionClass($class_name);
		$attrs = $reflClass->getAttributes();
		foreach ($attrs as $name => &$values) {
			$name = "Doctrine\\" . $name;
			$values = new $name(...$values);
		}
		return $attrs;
	}
}
 
namespace {
	<<ORM\Entity("user")>>
	class User {}
 
	var_dump(Doctrine\ORM\GetClassAttributes("User"));
}
?>
array(1) {
  ["ORM\Entity"]=>
  object(Doctrine\ORM\Entity)#2 (1) {
    ["name":"Doctrine\ORM\Entity":private]=>
    string(4) "user"
  }
}

Attributes with AST values may be used to implement “Design by Contract” and other verification paradigms as PHP extensions.

<<requires(
    $a >= 0,
    $b >= 0,
    $c >= 0,
    $a <= ($b+$c),
    $b <= ($a+$c),
    $c <= ($a+$b))>>
<<ensures(RET >= 0)>>
function triangleArea($a, $b, $c)
{
  $halfPerimeter = ($a + $b + $c) / 2;
 
  return sqrt($halfPerimeter
	* ($halfPerimeter - $a)
	* ($halfPerimeter - $b)
	* ($halfPerimeter - $c));
}

Special Attributes

Attribute names starting with “__” are reserved for internal purpose. Usage of unknown special attributes leads to compile-time error. Currently, no any special attributes are defined.

Criticism and Alternative Approach

Today we are using single doc-comments for any kind of meta-information, and many people don't see a benefit in the introduction of a special syntax. Everything may be grouped together and formatted using another special language.

/**
* Compute area of a triangle
*
* This function computes the area of a triangle using Heron's formula.
*
* @param number $a Length of 1st side
* @requires ($a >= 0)
* @param number $b Length of 2nd side
* @requires ($b >= 0)
* @param number $c Length of 3rd side
* @requires ($c >= 0)
* @requires ($a <= ($b+$c))
* @requires ($b <= ($a+$c))
* @requires ($c <= ($a+$b))
*
* @return number The triangle area
* @ensures (RET >= 0)
*
* @jit
*/
 
function triangleArea($a, $b, $c)
{
  $halfPerimeter = ($a + $b + $c) / 2;
 
  return sqrt($halfPerimeter
	* ($halfPerimeter - $a)
	* ($halfPerimeter - $b)
	* ($halfPerimeter - $c));
}

This approach works, but PHP itself doesn't have efficient access to pieces of this information. e.g. to check “jit” attribute, today, we would perform regular expression matching.

It might be possible to make PHP parse existing doc-comments and keep information as structured attributes, but we would need to invoke additional parser for each doc-comment; doc-comment may not conform to context-grammar and we have to decide what to do with grammar errors; finally this is going to be another language inside PHP.

Backward Incompatible Changes

The RFC doesn't make backward incompatibility changes, however, it makes forward incompatibility change. This means that frameworks that use native attributes won't be able to run on PHP versions lower than 7.1.

Proposed PHP Version(s)

7.1

RFC Impact

To SAPIs

None

To Existing Extensions

php-ast will require minor modification, because the patch moved classes “\ast\Node” and “\ast\Node\Decl” into core.

To Opcache

opcache modifications are parts of the proposed patch.

New Constants

None. However, we may move some constants from php-ast into core.

php.ini Defaults

None.

Open Issues

  • part of patch related to new AST classes (zend_ast.*) might need to be slightly changed to satisfy need of attributes and php-ast in best way.
  • getAttributes() should return empty array in case of no attributes [INCLUDED]
  • For each defined attribute getArray() should return a numerically indexed array independently of number of associated values. For attributes without values it should return empty arrays. [INCLUDED]
  • Attribute names might be namespace qualified e.g. <<\Foo\Bar>> [INCLUDED]
  • It may be useful to optionally allow some extra special character e.g. <<@\Foo\Bar>>. This character won't have any special meaning for PHP itself, but higher layer may use this “@” as a flag of special meaning.
  • May be we don't need special functionality for AST in attributes. We may store attribute as a simple strings and then get them through getAttributes() and call ast\parse_code() to get AST (if necessary). Both enabling and disabling native AST support make sense with their profs and cons. [ADDITIONAL VOTING QUESTION]
  • Naming: “Attributes” or “Annotation(s)”?

Proposed Voting Choices

This RFC modifies the PHP language syntax and therefore requires a two-third majority of votes

In addition, allow <php-expression> as attribute values using transparent AST representation. (1/2 majority required)

Patches and Tests

Implementation

After the project is implemented, this section should contain

  1. the version(s) it was merged to
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature

References

Rejected Features

Keep this updated with features that were discussed on the mail lists.

rfc/attributes.1461872801.txt.gz · Last modified: 2017/09/22 13:28 (external edit)