16 KiB
https://php.watch/articles/php-attributes
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
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.
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 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.
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.
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.
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.
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.
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(){}
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.
#[Attr('foo')]
function example(){}
#[Attr('foo')]
class Example {}
function example(#[Attr('foo')] string $foo) {}
class Foo {
#[Attr('foo')]
private string $foo;
}
class Foo {
#[Attr('foo')]
private const FOO = 'foo';
}
$fn = #[Attr('foo')] fn() => 1 > 2;
$fn = #[Attr('foo')] function() {
return 1 > 2;
}
$instance = new #[Attr('foo')] class {};
Attributes can be placed before and/or after DocBlock comments:
#[AttributeBefore('foo')]
#[AttributeBefore2('foo')]
#[AttrCommas('foo'), AttrCommas('foo')]
/**
* Foo
*/
#[AttributeAfter('foo')]
function example() {}
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"
).
#[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.
$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 {}
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 {}
}
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
).
ReflectionAttribute::newInstance
method returns an instance of the Attribute class, with any parameters passed to the Attribute object class constructor.
#[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)
// }
}
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]
.
When your project can afford to use PHP 8 as the minimum version, Doctrine-esque Annotations can be upgraded to first-class PHP Attributes.
class Book {
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.