rfc:attributes_v2

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
Last revisionBoth sides next revision
rfc:attributes_v2 [2020/04/14 11:21] beberleirfc:attributes_v2 [2020/05/04 12:00] beberlei
Line 1: Line 1:
 ====== PHP RFC: Attributes v2 ====== ====== PHP RFC: Attributes v2 ======
  
-  * Version: 0.4+  * Version: 0.5
   * Date: 2020-03-09   * Date: 2020-03-09
   * Author: Benjamin Eberlei (beberlei@php.net), Martin Schröder   * Author: Benjamin Eberlei (beberlei@php.net), Martin Schröder
-  * Status: Under Discussion+  * Status: Accepted 
 +  * Target: 8.0
   * First Published at: http://wiki.php.net/rfc/attributes_v2   * First Published at: http://wiki.php.net/rfc/attributes_v2
 +  * Implementation: https://github.com/php/php-src/pull/5394
  
 Large credit for this RFC goes to Dmitry Stogov whose previous work on  Large credit for this RFC goes to Dmitry Stogov whose previous work on 
Line 46: Line 48:
   * class methods   * class methods
   * function/method parameters   * function/method parameters
 +
 +Examples:
 +
 +<code php>
 +<<ExampleAttribute>>
 +class Foo
 +{
 +    <<ExampleAttribute>>
 +    public const FOO = 'foo';
 +
 +    <<ExampleAttribute>>
 +    public $x;
 +
 +    <<ExampleAttribute>>
 +    public function foo(<<ExampleAttribute>> $bar) { }
 +}
 +
 +$object = new <<ExampleAttribute>> class () { };
 +
 +<<ExampleAttribute>>
 +function f1() { }
 +
 +$f2 = <<ExampleAttribute>> function () { };
 +
 +$f3 = <<ExampleAttribute>> fn () => 1;
 +</code>
  
 Attributes are added before the declaration they belong to, similar to doc-block comments. They can be declared **before** or **after** a doc-block comment that documents a declaration. Attributes are added before the declaration they belong to, similar to doc-block comments. They can be declared **before** or **after** a doc-block comment that documents a declaration.
  
 <code php> <code php>
-<<...>> +<<ExampleAttribute>> 
-<<...>>+/** docblock */ 
 +<<AnotherExampleAttribute>>
 function foo() {} function foo() {}
 </code> </code>
Line 70: Line 99:
 The same attribute name can be used more than once on the same declaration. The same attribute name can be used more than once on the same declaration.
  
-Sementically the attribute declaration should be read as instantiating a class+Attributes can also be declared on the same line: 
 + 
 +<code php> 
 +<<WithoutArgument>><<SingleArgument(0)>><<FewArguments('Hello', 'World')>> 
 +function foo() {} 
 +</code> 
 + 
 +Semantically the attribute declaration should be read as instantiating a class
 with the attribute name and passing arguments to the constructor. with the attribute name and passing arguments to the constructor.
  
Line 89: Line 125:
 function foo() {} function foo() {}
 </code> </code>
 +
 +"The "Smiley" syntax uses the shorter, more familiar "at" symbol commonly seen in docblocks. The downside is that it does not permit whitespace in attribute names to allow detecting the ending of the declaration."
  
 See discussion about alternative syntaxes below for more info why the most See discussion about alternative syntaxes below for more info why the most
Line 166: Line 204:
 zend_ce_php_compiler_attribute = zend_register_internal_class(&ce); zend_ce_php_compiler_attribute = zend_register_internal_class(&ce);
  
-zend_attributes_internal_validator cb = zend_attribute_validate_phpcompilerattribute; +zend_compiler_attribute_register(zend_ce_php_compiler_attribute, zend_attribute_validate_phpcompilerattribute);
-zend_compiler_attribute_register(zend_ce_php_compiler_attribute, &cb);+
 </code> </code>
  
Line 208: Line 245:
  
 The constant AST is resolved to a value when accessing attributes with the Reflection API. The constant AST is resolved to a value when accessing attributes with the Reflection API.
 +**Note:** This is intentionally different from the previous Attributes RFC where an object
 +with ast\node was returned.
 +
 +The parser understands the context to differentiate attributes from bitshifts in constant ASTs.
 +
 +<code php>
 +<<BitShiftExample(4 >> 1, 4 << 1)>>
 +function foo() {}
 +</code>
  
 ==== Reflection ==== ==== Reflection ====
Line 234: Line 280:
  
 <code php> <code php>
-$attributes = $reflectionFunction->getAttributes(\My\Attributes\MyAbstractAttribute::class, \ReflectionAttribute::IS_INSTANCEOF);+$attributes = $reflectionFunction->getAttributes( 
 +    \My\Attributes\MyAbstractAttribute::class, 
 +    \ReflectionAttribute::IS_INSTANCEOF 
 +);
 </code> </code>
  
Line 244: Line 293:
     public function getName(): string     public function getName(): string
     public function getArguments(): array     public function getArguments(): array
-    public function getAsObject(): object+    public function newInstance(): object
 } }
 </code> </code>
Line 250: Line 299:
  
 Because validation of attributes is only performed during Because validation of attributes is only performed during
-//ReflectionAttribute::getAsObject()//, it is technically not required to+//ReflectionAttribute::newInstance()//, it is technically not required to
 declare the attribute class.  You can still acccess name and arguments directly declare the attribute class.  You can still acccess name and arguments directly
 from //ReflectionAttribute//. from //ReflectionAttribute//.
Line 279: Line 328:
     var_dump($attributes[0]->getName());     var_dump($attributes[0]->getName());
     var_dump($attributes[0]->getArguments());     var_dump($attributes[0]->getArguments());
-    var_dump($attributes[0]->getAsObject());+    var_dump($attributes[0]->newInstance());
 } }
  
Line 482: Line 531:
             foreach ($attributes as $listenerAttribute) {             foreach ($attributes as $listenerAttribute) {
                 /** @var $listener Listener */                 /** @var $listener Listener */
-                $listener = $listenerAttribute->getAsObject();+                $listener = $listenerAttribute->newInstance();
  
                 // with $listener instanceof Listener attribute,                 // with $listener instanceof Listener attribute,
Line 520: Line 569:
  
 Doctrine or any userland library can utilize the name filter with a parent class to fetch Doctrine or any userland library can utilize the name filter with a parent class to fetch
-only attributes they are interested in:+only attributes they are interested in. With the flexibility in the proposed Reflection API, Doctrine (or any other userland 
 +annotation/attributes library) can enforce stricter rules for use of the 
 +attributes by adding their own logic on top wihout PHP attributes getting in 
 +the way. 
 + 
 +Here is a complex example of an object using Doctrine Annotations and the proposed Attributes side by side to implement the same thing:
  
 <code php> <code php>
-namespace Doctrine\Annotations;+<?php 
 +use Doctrine\ORM\Attributes as ORM; 
 +use Symfony\Component\Validator\Constraints as Assert;
  
-abstract class Annotation { +<<ORM\Entity>> 
-    class TARGET_CLASS = 1; +/** @ORM\Entity */ 
-    class TARGET_PROPERTY = 2; +class User
- +
-    public $target = self::TARGET_CLASS; +
- +
-    final public function __construct(array $values = []) { +
-        foreach ($values as $key =$value) { +
-            $this->$key = $value; +
-        } +
-    } +
-} +
- +
-class AnnotationReader+
 { {
-    function getClassAnnotations(\ReflectionClass $reflection): array { +    /** @ORM\Id @ORM\Column(type="integer"*) @ORM\GeneratedValue */ 
-        $doctrineAnnotations = [];+    <<ORM\Id>><<ORM\Column("integer")>><<ORM\GeneratedValue>> 
 +    private $id;
  
-        foreach ($reflection->getAttributes() as $attribute) { +    /** 
-            // filter out any that doesn't extend Doctrine's annotation base class +     * @ORM\Column(type="string", unique=true
-            if (!is_subclass_of($attributeAnnotation::class)) { +     * @Assert\Email(message="The email '{{ value }}is not a valid email.") 
-                continue; +     */ 
-            }+    <<ORM\Column("string"ORM\Column::UNIQUE)>> 
 +    <<Assert\Email(array("message" => "The email '{{ value }}' is not a valid email."))>> 
 +    private $email;
  
-            $annotation $attribute->getAsObject();+    /** 
 +     * @ORM\Column(type="integer"
 +     * @Assert\Range( 
 +          min = 120, 
 +          max = 180, 
 +          minMessage = "You must be at least {{ limit }}cm tall to enter", 
 +          maxMessage = "You cannot be taller than {{ limit }}cm to enter" 
 +     * ) 
 +     */ 
 +    <<Assert\Range(["min" => 120, "max" => 180, "minMessage" => "You must be at least {{ limit }}cm tall to enter"])>> 
 +    <<ORM\Column(ORM\Column::T_INTEGER)>> 
 +    protected $height;
  
-            // validate that doctrine annotation is on the right "target+    /** 
-            // getClassAnnotations requires all annotations to be allowed on a class +     * @ORM\ManyToMany(targetEntity="Phonenumber") 
-            if ($annotation->target & Annotation::TARGET_CLASS === 0{ +     * @ORM\JoinTable(name="users_phonenumbers", 
-                throw new \RuntimeException(get_class($annotation) . is not allowed on class.")+          joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")}, 
-            +     *      inverseJoinColumns={@ORM\JoinColumn(name="phonenumber_id", referencedColumnName="id", unique=true)} 
- +     *      ) 
-            $doctrineAnnotations[] = $annotation; +     */ 
-        } +    <<ORM\ManyToMany(Phonenumber::class)>> 
- +    <<ORM\JoinTable("users_phonenumbers")>> 
-        return $doctrineAnnotations; +    <<ORM\JoinColumn("user_id", "id")>> 
-    }+    <<ORM\InverseJoinColumn("phonenumber_id", "id", JoinColumn::UNIQUE)>> 
 +    private $phonenumbers;
 } }
 </code> </code>
  
-With this flexibility in the Reflection API, Doctrine (or any other userland +The attributes approach is a bit limited, because it doesn't support named params. But this is the reason why attributes uses a function call like syntax, if the PHP language makes improvements with named params, then attributes would automatically benefit.
-annotation/attributes library) can also enforce stricter rules for use of the +
-attributes by adding their own logic on top wihout PHP attributes getting in +
-the way.+
  
 [[https://github.com/RectorPHP/Rector|Migration tools such as Rector]] can help with userland migrating to attributes. [[https://github.com/RectorPHP/Rector|Migration tools such as Rector]] can help with userland migrating to attributes.
Line 629: Line 686:
 full Doctrine like system is not necessary for a lot of use-cases, especially full Doctrine like system is not necessary for a lot of use-cases, especially
 the PHP internal use-cases. the PHP internal use-cases.
 +
 +=== Why are nested attributes not allowed? ===
 +
 +Nesting attributes means, defining an attribute as an argument to another attribute. This is intentionally not allowed because it would mean an attribute can be declared within an argument, which at the moment this RFC defines as constant AST. A constant AST is already known and re-usable and describes a subset of expressions that can be used in property, constant or argument default value declarations.
 +
 +Reusing constant AST concept for attribute arguments introduces consistency and the potential to benefit from future work on this concept. Allowing nested attributes would potentially cause a conflict in the future, as well as makes introducing complexity for users to understand this new context that behaves differently than other parts of the language.
  
 === Naming (attributes or annotations) === === Naming (attributes or annotations) ===
Line 687: Line 750:
   * Other languages such as Go have simple but powerful serialization from XML/JSON to objects and back. The combination of typed properties an attributes puts this in reach for core or a PHP extension to implement.   * Other languages such as Go have simple but powerful serialization from XML/JSON to objects and back. The combination of typed properties an attributes puts this in reach for core or a PHP extension to implement.
   * An alternative "short" syntax to declare attributes in one enclosing //<<SingleArgument("foo"), MultiArgument("bar", "baz")>>// This could be revisited in the future similar to grouped use statements being added after use statements already existed.   * An alternative "short" syntax to declare attributes in one enclosing //<<SingleArgument("foo"), MultiArgument("bar", "baz")>>// This could be revisited in the future similar to grouped use statements being added after use statements already existed.
-  * Extending userland attributes to allow declaring which target they are allowed to be declared on including validation of those targets in     //ReflectionAttribute::getAsObject()//.+  * Extending userland attributes to allow declaring which target they are allowed to be declared on including validation of those targets in     //ReflectionAttribute::newInstance()//.
  
-===== Proposed Voting Choices =====+===== Voting =====
  
-  * Accept PHP Attributes v2 into core? 2/3 majority +<doodle title="Accept PHP Attributes v2 into core?" auth="beberlei" voteType="single" closed="true"> 
-  * Which syntax to use for attributes? "<<>>" or "@:"+   Yes 
 +   * No 
 +</doodle> 
 + 
 +Secondary vote (choice with the most votes is picked): 
 + 
 +<doodle title=" Which syntax to use for attributes?" auth="beberlei" voteType="single" closed="true"> 
 +   <<>> 
 +   @: 
 +</doodle> 
 + 
 +Vote closes on May 4th, 12:00 UTC.
  
 ===== Patches and Tests ===== ===== Patches and Tests =====
Line 741: Line 815:
   * Changed validation of compiler attributes to use a C callback instead of instantiating object   * Changed validation of compiler attributes to use a C callback instead of instantiating object
   * Offer alternative syntax "@:" using new token T_ATTRIBUTE   * Offer alternative syntax "@:" using new token T_ATTRIBUTE
 +
 +0.5:
 +
 +  * Rename ReflectionAttribute::getAsObject to ReflectionAttribute::newInstance
  
rfc/attributes_v2.txt · Last modified: 2020/08/01 23:38 by carusogabriel