PHP 8.0: Attributes (3/4)

4 years ago by David Grudl  

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.