PHP 8: Attribute – Erklärung und Beispiel

In diesem Artikel möchte ich euch anhand eines kleinen Beispiels erklären wie ihr mit PHP 8 eure eigenen Attribute erstellen und nutzen könnt. Wir werden ein Attribut namens Route erstellen und es dann in einem Controller zur Definition der URL verwenden.

Falls du noch nichts von Attributen gehört hast: Sie dienen dazu die bisherigen Annotations zu ersetzen und sind mindestens genauso mächtig.

Ein eigenes Attribut erstellen

Ein Attribut ist zunächst eine ganz normale Klasse, die durch das Attribut #[\Attribute()] erst zu einem Attribut gemacht wird. Klingt verdreht, sieht aber dann recht unspektakulär aus:

namespace App\Attribute;

#[\Attribute(\Attribute::TARGET_METHOD)]
class Route
{
	public string $url;
	public string $name;

	// Mit dem Konstruktor definieren wir, welche Parameter das Attribut akzeptiert/benötigt.
	public function __construct(string $url, string $name)
	{
		$this->url  = $url;
		$this->name = $name;
	}
}

Der einzige Unterschied zu einer „normalen“ Klasse ist die Zeile
#[\Attribute(\Attribute::TARGET_METHOD)]
Wir können bei der Definition einige Einschränkungen zur Verwendbarkeit machen. Wir nutzen hier \Attribute::TARGET_METHOD drücken damit aus, dass unser Attribut an Methoden verwendet werden kann. Folgende Möglichkeiten habt ihr hier:

  • Attribute::TARGET_CLASS
  • Attribute::TARGET_FUNCTION
  • Attribute::TARGET_METHOD
  • Attribute::TARGET_PROPERTY
  • Attribute::TARGET_CLASS_CONSTANT
  • Attribute::TARGET_PARAMETER
  • Attribute::TARGET_ALL

Außerdem könnt ihr noch bestimmen, ob das Attribut ein oder mehrmals nebeneinander stehen darf. Standard ist einmal. Wenn ihr mehr erlauben wollt, müsst ihr Attribute::IS_REPEATABLE setzen.

Diese Flags sind binär codiert und ihr könnt mehrere kombinieren:
#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_CLASS|Attribute::IS_REPEATABLE)]
Nennt ihr kein TARGET_* Flag, dann gilt automatisch TARGET_ALL.

Achtung: Diese Einschränkungen werden erst geprüft, wenn ihr das Attribut instanziert und etwas damit machen wollt.

Die folgende Fehlermeldung erscheint, wenn das Attribut unerlaubterweise verwendet wird:
Fatal error: Uncaught Error: Attribute „…“ cannot target method (allowed targets: function)

Attribute verwenden

Den eigenen Code mit Attributen dekorieren

Unser Controller könnte das Attribut folgendermaßen verwenden:

namespace App;

use App\Attribute\Route;

class HomeController
{
	#[Route('/home', '_home')]
	public function home(): void
	{
		// ...
	}
}

Genau wie bei anderen Klassen auch, müsst ihr Route in den Namespace holen, wenn ihr nicht den FQCN verwenden wollt.

Die Parameter, die ihr bei der Definition mit angebt, müssen mit denen im Konstruktor übereinstimmen:
#[Route('/home', '_home')]
public function __construct(string $url, string $name) {}

Hier noch ein Beispiel für ein Attribut, das mit
#[\Attribute(\Attribute::TARGET_CLASS|\Attribute::IS_REPEATABLE)]
definiert wurde und somit Mehrfachnennung erlaubt:

#[MyAttribute()] 
#[MyAttribute()] 
class HomeController {}

Attribute auswerten

Alleine dadurch, dass wir ein Attribut an eine Methoden schreiben, passiert aber noch gar nichts. Ihr könnt sie an dieser Stelle als Datenobjekte auffassen. Diese entfalten selbst auch noch keine Wirkung, sondern benötigen einen Service, der mit ihnen arbeitet.

Zu unserem Beispiel passt wohl am besten ein Router, der in unserem Projekt alle Controller findet und sie nach ihren Routen befragt. Dieser könnte so aussehen:

class Router
{
	public function collectRoutes(): void
	{
		$reflect = new \ReflectionClass(\App\HomeController::class);
		// Unser Attribut unterstützt nur Attribute::TARGET_METHOD. Wir suchen also bei den Methoden danach.
		foreach ($reflect->getMethods() as $method) {
			// Uns interessiert hier nur Route, der Rest soll herausgefilter werden
			$attributes = $method->getAttributes(\App\Attribute\Route::class, \ReflectionAttribute::IS_INSTANCEOF);

			if (empty($attributes)) {
				// Es wird auch Methoden geben, die keine Route definieren. Diese überspringen wir.
				continue;
			}

			// Es darf immer nur eine Route geben - sie muss daher die erste im Array sein.
			$route = $attributes[0]->newInstance();

			// Ab hier unterscheidet sich unser Attribut nicht mehr von einem Datenobjekt.
			print_r($route);
			/*
				App\Attribute\Route Object
				(
					[url] => /home
					[name] => _home
				)
			 */
		}
	}
}

Zum Auswerten von Attributen erweitert PHP 8 die Klassen für Reflection. Erwähnenswert sind besonders Reflection*::getAttributes() und ReflectionAttribute.

ReflectionMethod::getAttributes() zum Beispiel liefert uns ein Array mit allen Attributen an einer Methode. Damit wir nicht selbst auf die für uns interessanten filtern müssen, erlaubt getAttributes() mit seinen beiden Parametern genau das. In unserem Beispiel wollen wir nur unser eigenes Attribut Route erhalten.

Derzeit gibt es keine weiteren Flags neben \ReflectionAttribute::IS_INSTANCEOF. Dieses besagt: Gib mir alle Attribute, die entweder die genannte Klasse selbst oder ein Kind davon sind.
Wir können den zweiten Parameter weglassen, dann vergleicht getAttributes() den Namen und erlaubt keine Kindklassen.

Da wir für Route die Mehrfachverwendung nicht erlaubt haben, muss es das erste im Array sein. Um damit arbeiten zu können nutzen wir ReflectionAttribute::newInstance().

Hier endet die Welt der Attribute. $route ist jetzt nichts weiter als ein Datenobjekt mit dem ihr eure Logiken füttern könnt.

Zusammenfassung

Attribute sind der Ersatz für Annotations und ihr werdet sie genau wie diese nicht alleine sondern als Teile einer größeren Komponente antreffen. Ihr Vorteil gegenüber Annotations ist dabei klar: Der Doc-Block wird auf seine ursprüngliche Verwendung – nämlich Dokumentation – zurückgeführt und die Auswertung kann getrennt erfolgen.