PHP 8.0: Attribute (3/4)

vor 3 Jahren von David Grudl  

Die PHP-Version 8.0 wird gerade veröffentlicht. Sie ist voll von neuen Dingen, wie keine andere Version zuvor. Ihre Einführung hat vier separate Artikel verdient. Im dritten Teil werden wir einen Blick auf Attribute werfen.

Attribute bieten eine völlig neue Möglichkeit, strukturierte Metadaten für Klassen und alle ihre Mitglieder sowie für Funktionen, Closures und ihre Parameter zu schreiben. PhpDoc-Kommentare wurden bisher für diesen Zweck verwendet, aber ihre Syntax war immer so locker und inkonsistent, dass es nicht möglich war, sie maschinell zu verarbeiten. Daher werden sie durch Attribute mit festgelegter Syntax und Unterstützung in Reflection-Klassen ersetzt.

Aus diesem Grund werden Bibliotheken, die bisher Metadaten durch das Parsen von phpDoc-Kommentaren abgerufen haben, diese durch Attribute ersetzen können. Ein Beispiel ist Nette, wo man in den neuesten Versionen von Application und DI bereits die Attribute Persistent, CrossOrigin und Inject anstelle der Annotationen @persistent, @crossOrigin und @inject verwenden kann.

Code mit Annotationen:

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

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

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

Dasselbe gilt für Attribute:

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 Attributnamen im Kontext von Namespaces und use -Klauseln genauso aus, als wären sie Klassen. Es wäre also sogar möglich, sie z.B. wie folgt zu 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. Aber es ist auf jeden Fall besser, wenn sie existiert, denn dann kann der Editor sie beim Schreiben vorschlagen, der statische Analysator erkennt Tippfehler usw.

Syntax

Es ist clever, dass PHP vor Version 8 Attribute nur als Kommentare sieht, so dass sie auch in Code verwendet werden können, der in älteren Versionen funktionieren sollte.

Die Syntax eines einzelnen Attributs sieht aus wie die Erstellung einer Objektinstanz, wenn wir den new Operator weglassen. Also, der Name der Klasse 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 ein Komma getrennt geschrieben werden können:

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

#[Inject, Lazy]
public Bar $bar;

Das folgende Attribut gilt für alle drei Eigenschaften:

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

Einfache Ausdrücke und Konstanten, die beim Kompilieren ausgewertet werden können und als Standardwerte für Eigenschaften verwendet werden, können als Argumente in Attributen verwendet werden:

#[
	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. Es gibt zum Beispiel keine einfache Möglichkeit, die folgende in Doctrine verwendete Anmerkung in Attribute umzuwandeln:

/**
 * @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.

Attribut Reflexion

Welche Attribute die einzelnen Elemente haben, kann mit Hilfe von Reflection ermittelt werden. Reflection-Klassen haben eine neue Methode getAttributes(), die ein Array von Objekten ReflectionAttribute zurückgibt.

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)
}

Die zurückgegebenen Attribute können durch einen Parameter gefiltert werden, z. B. $reflection->getAttributes(Example::class) gibt nur Attribute zurück Example.

Attribut-Klassen

Die Attributklasse MyAttributes\Example darf nicht existieren. Nur ein Methodenaufruf newInstance() setzt ihre Existenz voraus, weil er sie instanziiert. Also schreiben wir sie. Es wird eine ganz normale Klasse sein, nur müssen wir das Attribut Attribute bereitstellen (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 erlaubt sein soll. Zum Beispiel, nur für Klassen und Eigenschaften:

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

Es stehen die Flags TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER und standardmäßig TARGET_ALL zur Verfügung.

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

Die 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 seit seiner allerersten Version Annotationen verwendet, um persistente Parameter und Komponenten zu kennzeichnen, wurden sie nicht in größerem Umfang verwendet, weil sie kein natives Sprachkonstrukt waren, so dass die Redakteure sie nicht vorschlugen und man leicht einen Fehler machen konnte. Auch wenn dies bereits durch Editor-Plugins behoben wird, eröffnet der wirklich native Weg, der durch Attribute beschritten wird, völlig neue Möglichkeiten.

Attribute haben übrigens eine Ausnahme im Nette Coding Standard erhalten, der verlangt, dass der Klassenname neben der Spezifität (z.B. Product, InvalidValue) auch eine Generalität (z.B. ProductPresenter, InvalidValueException) enthält. Andernfalls wäre bei der Verwendung im Code nicht klar, was genau die Klasse repräsentiert. Bei Attributen ist dies nicht erwünscht, daher heißt die Klasse Inject statt InjectAttribute.

*Im letzten Teil werden wir uns ansehen, welche neuen Funktionen und Klassen in PHP aufgetaucht sind und den Just in Time Compiler vorstellen.