Attributes in PHP 8

Published On2020-05-29

Attributes in PHP 8

Attributes are finally in PHP 8! After years of discussions, feature requests, and user-land implementations such as Doctrine Annotations, Attributes proposal for PHP 8 is finally accepted!

This post is a detailed guide on Attributes, edge cases, and history, and practical guide on upgrading existing Annotations to Attributes.

TL;DR?
A shorter on-point post about the syntax, class synopsis, and a few examples is available at New in PHP 8: Attributes

Summary

What are Attributes

Attributes are small meta-data elements added for PHP classes, functions, closures, class properties, class methods, constants, and even on anonymous classes.

PHP DocBlock comments are probably the most familiar example.

/**
 * @param string $message
 */
function foo(string $message) {}

These comments are somewhat structured with @param "annotations". These small bits are not executed, but PHP provides an API called "Reflection API" to conveniently retrieve these comments.

This approach is a little brittle because it is easy to make a typo and that will go unnoticed until these comments are pulled from somewhere else in the code.

The new approach added in PHP 8 is Attributes. Attributes provide a more pragmatic approach to declare and fetch these little bits of information.

Frameworks such as Drupal, Symfony, and Doctrine use annotations to provide auxiliary information for certain classes in an organized way.

class AboutPage extends AbstractController {
    /**
     * @Route("/about")
     */
    public function page() {}
}

Piggybacking on DocBlock comments, this annotation provides useful information about the AboutPage class. From a framework, this can be turned into a router entry to route "/about" path to AboutPage::page method.

Attributes in PHP 8 goes steps ahead of this, which brings a structured and engine-validated approach to annotations.

class AboutPage extends AbstractController {
    <<Route('/about')>>
    public function page() {}
}

Instead of writing a separate definition in the form of an XML schema or a JSON schema, Attributes provide an easy and manageable way to organize this meta-data.

Attributes in other languages

Many languages have similar features to PHP Attributes.

  • Java is probably the most popular one, which has Annotations with a syntax similar to @Route(name = "/about").
  • Rust has Attributes with a syntax similar to #[route(name = "/about")].
  • Python annotations are called Decorators, and follow a similar syntax: @route("/about").

PHP's existing Doctrine-esque is widely used, but Attributes in PHP 8 uses the << and >> syntax, which is not used in other languages, but provides same semantic functionality nonetheless.

Attributes vs Annotations

Attributes and Annotations provide the same functionality. The word "Annotations" is already being used widely in PHP libraries and frameworks, so the name Attributes help to minimize the confusion with Annotations.

Previous Attempts

There were two previous attempts at bringing this feature to PHP.

The first one is about 8 years ago, with a proposal named "annotations".

In 2016, the first Attributes RFC was proposed by Dmitry Stogov.

Neither of these attempts were quite fruitful. The first Attributes RFC in fact proposed the same syntax we have for PHP 8, but the second RFC which made the cut to PHP 8 was a bit more elaborate and Benjamin Eberlei put an amazing effort to address minor details and to have a healthy discussion with the community to agree to the syntax and functionality.

Attributes Syntax and Features

The Attribute syntax is simply braces made with two smaller than and greater than signs.

<<Attribute>>

There was a good discussion and some bike-shedding when the syntax was being selected. A few alternative patterns suggested were:

  • @@ Attribute
  • [[Attribute]]
  • @: Attribute (voted out 41 : 12 in favor of <<Attribute>>)

Design Goals

PHP 8 Attributes provide convenient access to the information. The syntax and implementation aim to make the syntax quite familiar with what users are already familiar about:

  • Attributes may resolve to class names.
  • Attributes can be namespaced.
  • Attribute class names can be imported with use statements.
  • Attributes can have zero or more parameters to it.
  • There can be more than one Attribute to a declaration.
  • Attribute instances can be retrieved from the Reflection API.

All of these features are explained at the rest of this article with elaborate examples.

The use of namespaces and associating them with class names makes it easier to reuse and organize Attributes. They can be extended, and/or implement interfaces which the Reflection API provides a handy filter feature when Attributes are polled.

Attributes may resolve to class names

Although not required, PHP 8 provides functionality to resolve the attribute names to class names. You can use use statements to clean-up the code. Standard rules of class name resolving will be followed.

It is optional to match the Attribute name to a class name.

Attributes can have parameters

Each attribute can have zero or more parameters. They will be passed to the Attribute class constructor if attempted to get an instantiated object of the attribute.

Parameter can be simple scalar types, arrays, or even simple expressions such as mathematical expressions, PHP constants, class constants (including magic constants). Any expression that can be used as a class constant can be used as Attribute parameter.

More than one attribute allowed

Each item that receives Attributes can have zero or many attributes, each in its own <<>> brackets.

Each Attribute can be separated by a white-space (either a new line or a space(s)).

Before and After DocBlock comments

Attributes can appear before and after DocBlock comments. There is no standard recommendation for the code style, but this surely will be ironed out in a future PSR code-style recommendation.

Attribute Examples

Attributes can be added to a wide-range of declarations.

Functions

<<Attribute('foo')>>
function example(){}

Classes

<<Attribute('foo')>>
class Example {}

Function/Method Arguments

function example(<<Attribute('foo')>> string $foo) {}

Class Properties

class Foo {
    <<Attribute('foo')>>
    private string $foo;
}

Class Constants

class Foo {
    <<Attribute('foo')>>
    private const FOO = 'foo';
}

Closures

$fn = <<Attribute('foo')>> fn() => 1 > 2;

$fn = <<Attribute('foo')>> function() {
    return 1 > 2;
}

Anonymous Classes

$instance = new <<Attribute('foo')>> class {};

With DocBlocks

Attributes can be placed before and/or after DocBlock comments:

<<AttributeBefore('foo')>>
<<AttributeBefore2('foo')>>
/**
 * Foo
 */
<<AttributeAfter('foo')>>
function example() {}

Code Style

Because the syntax is still new, there is no PSR code-style agreed for Attributes.

A personal recommendation would be to:

  • Always place the Attributes after DocBlock comment.
  • Leave no spaces before and after the << and >> braces.
  • Follow the same style for function calls: Place a comma right after the parameter, and leave a space ("first", "second").

Complete Example


use App\Annotations\FooAttribute;
use App\Annotations\ClassAttrib as FooClassAttrib;
use App\Annotations\FooParamAttrib;
use External\Attr\BarClassAttrib;

<<App\Annotations\FooAttribute>>
function foo_func(<<FooParamAttrib('Foo1')>> $foo) {}

<<FooAttribute('hello')>>
<<BarClassAttrib(42)>>
class Foo {
    <<ConstAttr>>
    <<FooAttribute(null)>>
    private const FOO_CONST = 28;
    private const BAR_CONST = 28;

    <<PropAttr(Foo::BAR_CONST, 'string')>>
    private string $foo;

    <<SomeoneElse\FooMethodAttrib>>
    public function getFoo(<<FooClassAttrib(28)>>): string{}
}

Attribute Objects with <<PhpAttribute>>

As mentioned above, Attributes may resolve to PHP class names. You can create a class with the special <<PhpAttribute>> Attribute to declare one:


<<PhpAttribute>>
class FooAttribute {
    public function __construct(?string $param1 = null) {}
}

It's possible to declare this class in any namespace you wish. The parameters of the Attribute will be passed to the class constructor when a caller requests an instance of the Attribute class.

The Attribute declared above can now be used anywhere in an Attribute declaration:

<<FooAttribute('Hello World!')>>
function example() {}

Reflection API for Attributes

Attributes are retrieved using the Reflection API. When PHP engine parses code that contains Attributes, they are stored in internal structures for future use. Opcache support included. It does not execute any code or call the constructors of the attributes unless an instance of the Attribute is requested (see examples below).

Using the Reflection API, the Attributes can be retrieved either as strings that contain the Attribute name (with class names resolved), and its optional arguments.

Reflection API can also instantiate an instance of the Attribute class, with class names resolved, autoloaded, and the optional parameters passed to the class constructor. Failure to instantiate the class will throw \Error exceptions that can be caught at the caller level.

New Reflection*::getAttributes() method

$reflector = new \ReflectionClass(Foo::class);
$reflector->getAttributes();

All Reflection* classes get a new method getAttributes method, that returns an array of ReflectionAttribute objects. A synopsis of this new method would be similar to the following:

/**
 *  @param string $name Name of the class to filter the return list
 *  @param int $flags Flags to pass for the filtering process.
 *  @return array ReflectionAttribute[]
 */
public function getAttributes(?string $name = null, int $flags = 0): array {}

ReflectionAttribute class synopsis

final class ReflectionAttribute {
    /**
     * @return string The name of the attribute, with class names resolved.
     */
    public function getName(): string {}

    /**
     * @return array Arguments passed to the attribute when it is declared.
     */
    public function getArguments(): array {}

    /**
     * @return object An instantiated class object of the name, with arguments passed to the constructor.
     */
    public function newInstance(): object {}
}

Attribute filtering

Reflection*::getAttributes() optionally accepts a string of class name that can be used to filter the return array of attributes by a certain Attribute name.

$attrs = $reflector->getAttributes(FooAttribute::class);

$attrs array would now be only ReflectionAttribute objects or FooAttribute Attribute name.

A second optional parameter accepts an integer to further fine tune the return array.

$attrs = $reflector->getAttributes(BaseAttribute::class, \ReflectionAttribute::IS_INSTANCEOF);

At the moment, only \ReflectionAttribute::IS_INSTANCEOF is available.

If \ReflectionAttribute::IS_INSTANCEOF is passed, the return array will contain Attribute with same class name or classes that extends or implements the provided name (i.e. all classes that fulfill instanceOf $name).

Retrieving Attribute Object Instances

ReflectionAttribute::newInstance method returns an instance of the Attribute class, with any parameters passed to the Attribute object class constructor.

Complete Reflection Example


use My\Attributes\ExampleAttribute;

<<ExampleAttribute('Hello world', 42)>>
class Foo {}

<<PhpAttribute>>
class ExampleAttribute {
    private string $message;
    private int $answer;
    public function __construct(string $message, int $answer) {
        $this->message = $message;
        $this->answer = $answer;
    }
}

$reflector = new \ReflectionClass(Foo::class);
$attrs = $reflector->getAttributes();

foreach ($attrs as $attribute) {

    $attribute->getName(); // "My\Attributes\ExampleAttribute"
    $attribute->getArguments(); // ["Hello world", 42]
    $attribute->newInstance();
        // object(ExampleAttribute)#1 (2) {
        //  ["message":"Foo":private]=> string(11) "Hello World"        
        //  ["answer":"Foo":private]=> int(42) 
        // }
}

Practical Use Cases

The Attributes feature is quite powerful because they can be directly associated with class names, and class name resolution is built-in, static analyzers and IDEs will be able easily add support for Attributes.

The syntax sure will sure be unfamiliar for those who used Attributes/Annotations in Java or Rust. This is small price to pay for implementation details in the RFC. Better awkward syntax than never, right?

Note that Attributes are not backwards-compatible. Due to the syntax being new, PHP versions prior to 8.0 will trigger parse errors:

Parse error: syntax error, unexpected '<<' (T_SL), expecting end of file in ... on line ...

Migrating from Doctrine Annotations to Attributes

When your project can afford to use PHP 8 as the minimum version, Doctrine-esque Annotations can be upgraded to first-class PHP Attributes.

- /** @ORM\Entity */
+ <<ORM\Entity>>
  class Book {
-   /** 
-    * @ORM\Id
-    * @ORM\Column(type="string")
-    * @ORM\GeneratedValue
-    */
+   <<ORM\Id>>
+   <<ORM\Column("string")>>
+   <<ORM\GeneratedValue>>
    private string $isbn;
  }

Attributes in the Future

Attributes can be the corner-stone for many PHP functionality that are not ideally "marked" with an interface.

In the proposal for Attributes, it mentions using Attributes to mark declarations compatible/incompatible for JIT.

Another use case is a <<Deprecated>> Attribute that can be used to declare both engine and user-land classes/functions or anything else as deprecated. This can eventually retire the @deprecated DocBlock comments.

Drupal and Symfony use Doctrine Annotation for controllers, plugins, render blocks, etc. All of them can be upgraded to Attributes when the time is right.

Further resources

Recent Articles on PHP.Watch

All Articles β€’ Feed
PHP 8.0 Thanks ❀

PHP 8.0 Thanks ❀

PHP 8.0.0 is released today πŸŽ‰πŸΎπŸŽŠ. Thank you all of you for your amazing efforts ❀.
PHP's resource to object transformation

PHP's resource to object transformation

A summary of PHP's long-term progress in `resource` objects to class objects
PHP Hash Algorithm Benchmark

PHP Hash Algorithm Benchmark

Benchmarks the performance of hashing algorithms supported in PHP, including MurmurHash in PHP 8.1.
Subscribe to PHP.Watch newsletter for monthly updates

You will receive an email on last Saturday of every month and on major PHP releases with new articles related to PHP, upcoming changes, new features and what's changing in the language. No marketing emails, no selling of your contacts, no click-tracking, and one-click instant unsubscribe from any email you receive.