PHP 8.0: Attributes (3/4)
PHP version 8.0 is being released right now. It's full of new stuff like no other version before. Their introduction deserved four separate articles. In the third part we'll take a look at attributes.
Attributes provide a whole new way to write structured metadata for classes and all its members, as well as functions, closures, and their parameters. PhpDoc comments have been used for this purpose so far, but their syntax has always been so loose and inconsistent that it was not possible to start machine processing them. Therefore, they are being replaced by attributes with determined syntax and support in reflection classes.
Because of that, libraries that have previously retrieved metadata by parsing
phpDoc comments will be able to replace them with attributes. One example is
Nette, where in the latest versions of Application and DI you can already use
the attributes Persistent
, CrossOrigin
and
Inject
instead of annotations @persistent
,
@crossOrigin
and @inject
.
Code using annotations:
/**
* @persistent(comp1, comp2)
*/
class SomePresenter
{
/** @persistent */
public $page = 1;
/** @inject */
public Facade $facade;
/**
* @crossOrigin
*/
public function handleSomething()
{
}
}
The same with attributes:
use Nette\Application\Attributes\CrossOrigin;
use Nette\Application\Attributes\Persistent;
use Nette\DI\Attributes\Inject;
#[Persistent('comp1', 'comp2')]
class SomePresenter
{
#[Persistent]
public int $page = 1;
#[Inject]
public Facade $facade;
#[CrossOrigin]
public function handleSomething()
{
}
}
PHP evaluates attribute names the same as if they were classes, in the
context of namespaces and use
clauses. So it would be even possible
to write them, for example, as follows:
use Nette\Application\Attributes;
class SomePresenter
{
#[Attributes\Persistent]
public int $page = 1;
#[\Nette\DI\Attributes\Inject]
public Facade $facade;
The class representing the attribute may or may not exist. But it’s definitely better if it exists, because then the editor can suggest it during writing, the static analyzer recognizes typos, etc.
Syntax
It’s clever that PHP before version 8 sees attributes only as comments, so they can also be used in code that should work in older versions.
The syntax of an individual attribute looks like creating an object instance
if we omit the new
operator. So, the name of the class followed by
optional arguments in parentheses:
#[Column('string', 32, true, false)]#
protected $username;
And here’s the place where the new hot feature of PHP 8.0 can be put to use – named arguments:
#[Column(
type: 'string',
length: 32,
unique: true,
nullable: false,
)]#
protected $username;
Each element can have multiple attributes that can be written individually or separated by a comma:
#[Inject]
#[Lazy]
public Foo $foo;
#[Inject, Lazy]
public Bar $bar;
The following attribute applies to all three properties:
#[Common]
private $a, $b, $c;
Simple expressions and constants, that can be evaluated during compilation and are used as default values for properties, can be used as arguments in attributes:
#[
ScalarExpression(1 + 1),
ClassNameAndConstants(PDO::class, PHP_VERSION_ID),
BitShift(4 >> 1, 4 << 1),
BitLogic(1 | 2, JSON_HEX_TAG | JSON_HEX_APOS),
]
Unfortunately, the value of an argument can’t be another attribute, i.e. attributes can’t be nested. For example, there is no straightforward way for the following annotation used in Doctrine to be converted into attributes:
/**
* @Table(name="products",uniqueConstraints={@UniqueConstraint(columns={"name", "email"})})
*/
Also, there is no attribute equivalent for the file phpDoc, i.e. a comment located at the beginning of a file, that is used, for example, by Nette Tester.
Attribute reflection
What attributes the individual elements have can be determined by using
reflection. Reflection classes have a new getAttributes()
method,
that returns an array of objects ReflectionAttribute
.
use MyAttributes\Example;
#[Example('Hello', 123)]
class Foo
{}
$reflection = new ReflectionClass(Foo::class);
foreach ($reflection->getAttributes() as $attribute) {
$attribute->getName(); // full attribute name, e.g. MyAttributes\Example
$attribute->getArguments(); // ['Hello', 123]
$attribute->newInstance(); // returns an instance: new MyAttributes\Example('Hello', 123)
}
Returned attributes can be filtered by a parameter, e.g.
$reflection->getAttributes(Example::class)
returns only
attributes Example
.
Attribute classes
Attribute class MyAttributes\Example
may not exist. Only a
method call newInstance()
requires its existence because it
instantiates it. So let's write it. It’ll be a completely ordinary class,
only we have to provide the attribute Attribute
(i.e. from the
global system namespace):
namespace MyAttributes;
use Attribute;
#[Attribute]
class Example
{
public function __construct(string $message, int $number)
{
...
}
}
It can be restricted for which language elements the usage of the attribute will be permitted. For example, just for classes and properties:
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class Example
{
...
}
Flags TARGET_CLASS
, TARGET_FUNCTION
,
TARGET_METHOD
, TARGET_PROPERTY
,
TARGET_CLASS_CONSTANT
, TARGET_PARAMETER
and default
TARGET_ALL
are available.
But beware, the verification of correct or incorrect usage occurs
surprisingly only when the newInstance()
method is called. The
compiler itself does not perform this check.
The future with attributes
Thanks to attributes and new types,
PHP documentation comments for the first time in their history will really
become only documentary comments. PhpStorm already comes with custom
attributes, which can replace, for example, annotation
@deprecated
. And it can be assumed that this attribute will be one
day in PHP by default. Similarly, other annotations will be replaced, such as
@throws
etc.
Although Nette has been using annotations since its very first version to indicate persistent parameters and components, they have not been used more massively because they were not a native language construct, so editors did not suggest them and it was easy to make a mistake. Even though this is already being addressed by editor plugins, the really native way, which is being brought by attributes, opens up completely new possibilities.
By the way, attributes have gained an exception in the Nette Coding Standard,
which requires the class name, in addition to specificity (e.g.
Product
, InvalidValue
), to also contain a generality
(i.e. ProductPresenter
, InvalidValueException
).
Otherwise, when used in code, it would not be clear what exactly the class
represents. For attributes, this is not desirable, so the class is called
Inject
instead of InjectAttribute
.
In the last part, we'll look at what new functions and classes have appeared in PHP and introduce the Just in Time Compiler.
Sign in to submit a comment