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")], that is exactly the same as PHP's implementation.
  • 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 ] brace syntax. This was debated and changed from the initial <<Attr>> implementation to @@Attr to the final #[Attr] syntax.

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 #[ and ]

#[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>>)

The initial <<Attr>> syntax was changed to @@ by an RFC later, followed by yet another RFC to change to #[, ], that brings some form of backwards compatibility too.

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.

If an attribute does not map to a class name, that attribute is allowed to be repeated, and does not allow to be instantiated from the Reflection API.

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, or separate by a comma.

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

Note that if an attribute maps to a class name, that attribute is not allowed to attributed more than once. The attribute can be declared explicitly as repeatable to allow this.

#[Attr]
#[FooAttr]
function foo(){}

#[Attr, FooAttr]
function bar(){}

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

#[Attr('foo')]
function example(){}

Classes

#[Attr('foo')]
class Example {}

Function/Method Arguments

function example(#[Attr('foo')] string $foo) {}

Class Properties

class Foo {
    #[Attr('foo')]
    private string $foo;
}

Class Constants

class Foo {
    #[Attr('foo')]
    private const FOO = 'foo';
}

Closures

$fn = #[Attr('foo')] fn() => 1 > 2;

$fn = #[Attr('foo')] function() {
    return 1 > 2;
}

Anonymous Classes

$instance = new #[Attr('foo')] class {};

With DocBlocks

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

#[AttributeBefore('foo')]
#[AttributeBefore2('foo')]
#[AttrCommas('foo'), AttrCommas('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

#[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)] $a): string{}
}

// Declare Attributes

/*
 * Attributes are declared with `#[Attribute]`.
 */

#[Attribute]
class FooAttribute {
    public function __construct(?string $param1 = null) {}
}

#[Attribute]
class ClassAttrib {
    public function __construct(int $index) {}
}

Declaring Attributes

The attribute itself may be declared as a class. This is validated only when the attribute is fetched, and not immediately when the code is parsed.

A PHP attribute is a standard PHP class, declared with #[Attribute] attribute.

#[Attribute]
class FooAttribute{

}

By default, a declared attribute can be used on any item that accepts attributes. This includes classes, class methods, closures, functions, parameters, and class properties.

When declaring the attribute, it is possible to declare the targets the attribute must be used.

#[Attribute(Attribute::TARGET_CLASS)]
class Foo {}

When the attribute is attributed with the targets it supports, PHP does not allow the attribute to be used on any other targets. It accepts a bit-mask to allow the attribute in one or more targets.

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Foo {}

It allows the following targets:

  • Attribute::TARGET_ALL
  • Attribute::TARGET_FUNCTION
  • Attribute::TARGET_METHOD
  • Attribute::TARGET_PROPERTY
  • Attribute::TARGET_CLASS_CONSTANT
  • Attribute::TARGET_PARAMETER
  • Attribute::TARGET_METHOD

TARGET_ALL is the OR of all other targets.

Attribute class is declared final

The Attribute class is declared final, which prevents it from being extended.

Repeatable Attributes

By default, it is not allowed to use the same attribute on the same target more than once. The attribute must explicitly allow it:

#[Attribute(Attribute::IS_REPEATABLE)]
class MyRepeatableAttribute{}

Attribute class synopsis

#[Attribute(Attribute::TARGET_CLASS)]
final class Attribute {
    public int $flags;
    /**
     * Marks that attribute declaration is allowed only in classes.
     */
    const TARGET_CLASS = 1;

    /**
     * Marks that attribute declaration is allowed only in functions.
     */
    const TARGET_FUNCTION = 1 << 1;

    /**
     * Marks that attribute declaration is allowed only in class methods.
     */
    const TARGET_METHOD = 1 << 2;

    /**
     * Marks that attribute declaration is allowed only in class properties.
     */
    const TARGET_PROPERTY = 1 << 3;

    /**
     * Marks that attribute declaration is allowed only in class constants.
     */
    const TARGET_CLASS_CONSTANT = 1 << 4;

    /**
     * Marks that attribute declaration is allowed only in function or method parameters.
     */
    const TARGET_PARAMETER = 1 << 5;

    /**
     * Marks that attribute declaration is allowed anywhere.
     */
    const TARGET_ALL = (1 << 6) - 1;

    /**
     * Notes that an attribute declaration in the same place is
     * allowed multiple times.
     */
    const IS_REPEATABLE = 1 << 10;

    /**
     * @param int $flags A value in the form of a bitmask indicating the places
     * where attributes can be defined.
     */
    public function __construct(int $flags = self::TARGET_ALL)
    {
    }
}

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, auto-loaded, 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

#[exampleAttribute('Hello world', 42)]
class Foo {}

#[Attribute]
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.

IDEs such as PHPStorm already support Attributes, and it even offers a few built-in attributes of its own, such as #[Deprecated].

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 ArticlesFeed
PHP's new convention on using namespaces for extensions

PHP's new convention on using namespaces for extensions

A recent RFC passed in favor of using a namespace for PHP extensions.
How to compile PHP from source on Debian/Ubuntu - Beginner's guide

How to compile PHP from source on Debian/Ubuntu - Beginner's guide

Beginner's guide to compile PHP from the source, on Ubuntu/Debian with up to date instructions for PHP 8 and up.
Function Inlining in Zend Engine

Function Inlining in Zend Engine

A list of special PHP functions that Zend Engine can inline and optimize.
Subscribe to PHP.Watch newsletter for monthly updates

You will receive an email on last Wednesday 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.