PHP 8.0: Attributes
One of the biggest new changes in PHP 8 is the Attributes support. Attributes help to add meta-data to PHP functions, parameters, classes, class methods, constants, properties, closures, and even anonymous classes. This meta-data can be fetched programmatically, and provides a pragmatic approach to resolve the attributes in elsewhere in the code.
An in-depth post about the history, different approaches, and practical use cases of Attributes are in the post Attributes in PHP 8.
Syntax and Usage
Attributes are declared with #[
and ]
braces.
#[ExampleAttribute('foo', 'bar')]
function example() {}
Attributes syntax was initially implemented as
<<Attribute>>
, followed by@@Attribute
, and finally as#[Attribute]
.
In all PHP versions, #
starts a comment for the rest of the line. When an attribute is used in its own line, it will not cause a syntax error in PHP versions prior to 8.0. This makes code that uses PHP 8.0 attributes somewhat backwards compatible, as long as each attribute is attributed in its own line, which makes PHP ignore those attributes.
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, the Reflection API does not allow to fetch an instance of the instantiated attribute.
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 inside the same brackets separated by a comma.
Each Attribute can be separated by a white-space (either a new line or a space(s)).
#[Attr]
#[FooAttr]
function foo(){}
#[Attr, FooAttr]
function bar(){}
By default, the same attribute is not allowed to be used more than once. However, it is possible to declare an attribute that explicitly allows repeated use.
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.
Declaring Attributes
In order to use 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 Syntax 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) {}
}
Looking for fine-grained examples ? Attributes in PHP 8 contains several more examples with edge-cases demonstrated.
Fetching 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 fulfull instanceOf $name
).
Retrieving Attribute Objects
ReflectionAttribute::newInstance
method returns an instance of the Attribute class, with any parameters passed to the Attribute object class constructor.
A complete 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 $attriubute) {
$attriubute->getName(); // "My\Attributes\ExampleAttribute"
$attriubute->getArguments(); // ["Hello world", 42]
$attriubute->newInstance();
// object(ExampleAttribute)#1 (2) {
// ["message":"Foo":private]=> string(11) "Hello World"
// ["answer":"Foo":private]=> int(42)
// }
}
Read in-depth: This is a short summary of the new Attributes feature in PHP 8. For a detailed guide, see Attributes in PHP 8
Backwards Compatibility Impact
The final syntax of Attributes feature is not backwards-compatible. However, because the #
character is a valid symbol to start a comment, an attribute declared on its own line can make the code parse and run on earlier PHP versions.
#[ExampleAttribute('Hello world', 42)]
class Foo {}
If an attribute is used within the same line, the parser of prior PHP versions will not work:
function foo(#[TestAttr] $bar){
}
Parse error: syntax error, unexpected '}', expecting variable (T_VARIABLE) in ... on line ...