rfc:attributes

This is an old revision of the document!


PHP RFC: Attributes

Introduction

Attributes (or annotation) is 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 has to parse it to access particular element of that structure.

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

Proposal

Attribute Syntax

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 should be placed before the corresponding definition, but in opposite, it is possible to define few attributes for the same declaration.

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

Each attribute definition construct may also define one or few named attributes, each attribute may be used without value, with single value or multiple values. See 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 for the same definition few times, however it's possible to use multiple attribute values.

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

Arbitrary PHP Expressions as Attribute Values (AST attributes)

Except for 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 attribute is kept as an Abstract Syntax Tree, and we will able to read every individual node of this tree separately. This approach implies usage of the same PHP syntax for meta data and eliminates need for separate parser.

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

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

Reflection

Few reflection classes are extended with getAttributes() method, hat 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 array with attribute names as keys and nested arrays as corresponding values. Attributes without values represented by empty arrays, attributes with single value by arrays with 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 and 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 check and special handling in extensions.

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

Attributes may be used provide 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 use single doc-comments for any kind of meta-information, and many people don't see a benefit in introduction of the special syntax. Everything may be grouped together and formatted usng 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 dosn'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]

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.1461593842.txt.gz · Last modified: 2017/09/22 13:28 (external edit)