PHP 8.0: Attribute (3/4)

vor 4 Jahren von David Grudl  

PHP Version 8.0 ist erschienen. Sie ist so vollgepackt mit Neuerungen wie keine Version zuvor. Ihre Vorstellung erforderte gleich vier separate Artikel. In diesem dritten Teil werfen wir einen Blick auf Attribute.

Attribute bringen eine völlig neue Art und Weise, strukturierte Metadaten zu Klassen und all ihren Mitgliedern, sowie zu Funktionen, Closures und deren Parametern zu schreiben. Zu diesem Zweck wurden bisher phpDoc-Kommentare verwendet, aber ihre Syntax war immer so frei und uneinheitlich, dass es nicht möglich war, sie maschinell zu verarbeiten. Daher werden sie durch Attribute mit fester Syntax und Unterstützung in Reflection-Klassen ersetzt.

Daher können Bibliotheken, die bisher Metadaten durch Parsen von phpDoc-Kommentaren gewonnen haben, diese durch Attribute ersetzen. Ein Beispiel dafür ist Nette, wo Sie in den neuesten Versionen von Application und DI bereits statt der Annotationen @persistent, @crossOrigin und @inject die Attribute Persistent, CrossOrigin und Inject verwenden können.

Code mit Annotationen:

/**
 * @persistent(comp1, comp2)
 */
class SomePresenter
{
	/** @persistent */
	public $page = 1;

	/** @inject */
	public Facade $facade;

	/**
	 * @crossOrigin
	 */
	public function handleSomething()
	{
	}
}

Dasselbe mit Attributen:

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 wertet die Namen von Attributen genauso aus, als wären es Klassen, also im Kontext des Namensraums und der use-Klauseln. Man könnte sie also beispielsweise auch so schreiben:

use Nette\Application\Attributes;

class SomePresenter
{
	#[Attributes\Persistent]
	public int $page = 1;

	#[\Nette\DI\Attributes\Inject]
	public Facade $facade;

Die Klasse, die das Attribut repräsentiert, kann existieren oder auch nicht. Es ist aber definitiv besser, wenn sie existiert, da der Editor sie dann beim Schreiben vorschlagen kann, der statische Analysator Tippfehler erkennt usw.

Syntax

Praktisch ist, dass PHP vor Version 8 Attribute nur als Kommentare sieht, sodass sie auch in Code verwendet werden können, der in älteren Versionen funktionieren soll.

Die Schreibweise eines einzelnen Attributs sieht aus wie die Erstellung einer Objektinstanz, wenn wir den new-Operator weglassen würden. Also der Klassenname, gefolgt von optionalen Argumenten in Klammern:

#[Column('string', 32, true, false)]#
protected $username;

Und hier kommt das neue Feature von PHP 8.0 zum Einsatz – benannte Argumente:

#[Column(
	type: 'string',
	length: 32,
	unique: true,
	nullable: false,
)]#
protected $username;

Jedes Element kann mehrere Attribute haben, die einzeln oder durch Komma getrennt geschrieben werden können:

#[Inject]
#[Lazy]
public Foo $foo;

#[Inject, Lazy]
public Bar $bar;

Das folgende Attribut gilt für alle drei Properties:

#[Common]
private $a, $b, $c;

In den Standardwerten von Properties können einfache Ausdrücke und Konstanten verwendet werden, die während der Kompilierung ausgewertet werden können, und dasselbe gilt auch für die Argumente von Attributen:

#[
	ScalarExpression(1 + 1),
	ClassNameAndConstants(PDO::class, PHP_VERSION_ID),
	BitShift(4 >> 1, 4 << 1),
	BitLogic(1 | 2, JSON_HEX_TAG | JSON_HEX_APOS),
]

Leider kann der Wert eines Arguments kein weiteres Attribut sein, d.h. Attribute können nicht verschachtelt werden. Zum Beispiel lässt sich die folgende in Doctrine verwendete Annotation nicht ganz direkt in Attribute umwandeln:

/**
 * @Table(name="products",uniqueConstraints={@UniqueConstraint(columns={"name", "email"})})
 */

Es gibt auch kein Attribut-Äquivalent für die Datei phpDoc, d. h. einen Kommentar am Anfang einer Datei, der z. B. von Nette Tester verwendet wird.

Ermitteln von Attributen

Welche Attribute einzelne Elemente haben, ermitteln wir mittels Reflection. Reflection-Klassen verfügen über die neue Methode getAttributes(), die ein Array von ReflectionAttribute-Objekten zurückgibt.

use MyAttributes\Example;

#[Example('Hallo', 123)]
class Foo
{}

$reflection = new ReflectionClass(Foo::class);

foreach ($reflection->getAttributes() as $attribute) {
	$attribute->getName();      // vollständiger Attributname, z.B. MyAttributes\Example
	$attribute->getArguments(); // ['Hallo', 123]
	$attribute->newInstance();  // gibt Instanz new MyAttributes\Example('Hallo', 123) zurück
}

Die zurückgegebenen Attribute können nach Parametern gefiltert werden, z.B. gibt $reflection->getAttributes(Example::class) nur Attribute vom Typ Example zurück.

Attributklassen

Die Attributklasse MyAttributes\Example muss nicht existieren. Ihre Existenz erfordert nur der Aufruf der Methode newInstance(), da diese ihre Instanz erstellt. Schreiben wir sie also. Es handelt sich um eine ganz normale Klasse, nur müssen wir bei ihr das Attribut Attribute angeben (d.h. aus dem globalen System-Namensraum):

namespace MyAttributes;

use Attribute;

#[Attribute]
class Example
{
	public function __construct(string $message, int $number)
	{
		...
	}
}

Es kann eingeschränkt werden, für welche Sprachelemente die Verwendung des Attributs legal ist. So zum Beispiel nur für Klassen und Properties:

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class Example
{
	...
}

Zur Verfügung stehen die Flags TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER und der Standard TARGET_ALL.

Aber Achtung, die Überprüfung der korrekten oder falschen Verwendung erfolgt überraschenderweise erst beim Aufruf der Methode newInstance(). Der Compiler selbst führt diese Prüfung nicht durch.

Zukunft mit Attributen

Dank Attributen und neuen Typen werden PHP-Dokumentationskommentare zum ersten Mal in ihrer Geschichte wirklich nur noch dokumentarische Kommentare sein. PhpStorm verfügt bereits über benutzerdefinierte Attribute, die z.B. die Annotation @deprecated ersetzen können. Und es ist davon auszugehen, dass dieses Attribut eines Tages in PHP standardmäßig vorhanden sein wird. In ähnlicher Weise werden auch andere Annotationen ersetzt werden, wie z. B. @throws usw.

Obwohl Nette Annotationen seit seiner allerersten Version zur Kennzeichnung persistenter Parameter und Komponenten verwendet, kam es nicht zu ihrer massiveren Nutzung, da es sich nicht um ein natives Sprachkonstrukt handelte, sodass Editoren sie nicht vorschlugen und es leicht war, Fehler darin zu machen. Dies lösen zwar heute Plugins für Editoren, aber der wirklich native Weg, den Attribute bringen, eröffnet völlig neue Möglichkeiten.

Übrigens haben Attribute eine Ausnahme im Nette Coding Standard erhalten, der fordert, dass der Klassenname neben der Spezifität (z.B. Product, InvalidValue) auch die Allgemeinheit enthält (also ProductPresenter, InvalidValueException). Andernfalls wäre bei der Verwendung im Code nicht ersichtlich, was die Klasse genau darstellt. Bei Attributen ist dies hingegen nicht erwünscht, daher heißt die Klasse Inject anstelle von InjectAttribute.

Im letzten Teil werfen wir einen Blick darauf, welche neuen Funktionen und Klassen in PHP aufgetaucht sind und stellen den Just-in-Time-Compiler vor.