von der Docblock-Konvention zur nativen Sprachfunktion
Eigene PHP-Attribute ersetzen Docblock-Annotationen durch eine vom Compiler geparste, typsichere Syntax: Eine mit #[Attribute] markierte Klasse definiert die Struktur, Attribute::TARGET-Flags schränken das erlaubte Ziel ein, und eine ReflectionClass liest die Daten zur Laufzeit aus, ganz ohne fragilen String-Parser für Kommentare.
Inhaltsverzeichnis
- 1. Warum eigene PHP-Attribute: von Annotationen zu nativer Syntax
- 2. Eine Attribut-Klasse mit #[Attribute] definieren
- 3. Ziel einschränken mit Attribute::TARGET und Flags kombinieren
- 4. PHP-Attribute per Reflection auslesen
- 5. Konstruktor-Argumente, benannte Parameter und Default-Werte
- 6. Ein eigenes Validierungssystem auf Basis von PHP-Attributen bauen
- 7. Mehrfach-Attribute mit IS_REPEATABLE verarbeiten
- 8. PHP-Attribute vs. Interfaces und Docblocks: wann was
- 9. Performance-Overhead von Reflection und Caching-Strategien
- 10. Zusammenfassung
- 11. FAQ
1. Warum eigene PHP-Attribute: von Annotationen zu nativer Syntax
Vor PHP 8.0 wurden Metadaten an Klassen, Methoden und Properties fast ausschließlich über Docblock-Annotationen wie @Route("/api/orders") transportiert. Diese Annotationen waren aus Sicht des PHP-Parsers reine Kommentare, ohne jede Syntaxprüfung. Frameworks mussten eigene Parser schreiben, die Kommentartext mit regulären Ausdrücken zerlegten, was bei Tippfehlern erst zur Laufzeit auffiel, nie beim Schreiben des Codes. Eigene PHP-Attribute lösen genau dieses Problem.
Seit PHP 8.0 sind Attribute Teil der Sprachsyntax und werden vom Compiler selbst geparst, nicht von einem externen Regex-Parser. Ein Tippfehler in der Attribut-Syntax führt zu einem echten Parse-Fehler, sichtbar sofort in der IDE, nicht erst bei der Ausführung. Damit werden eigene PHP-Attribute vom reinen Kommentar zu einem vollwertigen, typsicheren Sprachkonstrukt, das dieselben Regeln für Typen, Konstruktoren und Sichtbarkeit befolgt wie jede andere Klasse auch.
Der praktische Nutzen zeigt sich überall dort, wo Konfiguration nah am Code stehen soll: Routing, Validierungsregeln, Serialisierungshinweise oder Zugriffskontrolle. Statt einer separaten YAML- oder XML-Datei steht die Information direkt über der Methode oder Property, die sie betrifft, lesbar für Menschen und maschinell auswertbar über Reflection, ohne zusätzliche Bibliothek für das Parsing.
2. Eine Attribut-Klasse mit #[Attribute] definieren
Der erste Schritt zu eigenen PHP-Attributen ist eine gewöhnliche Klasse, die selbst mit #[Attribute] markiert wird. Diese Markierung teilt PHP mit, dass Instanzen dieser Klasse als Attribut über anderem Code verwendet werden dürfen. Ohne diese Markierung wirft PHP beim Versuch, die Klasse als Attribut zu verwenden, einen Error, denn nicht jede Klasse soll sich für diesen Zweck eignen.
Die Attribut-Klasse selbst folgt keinen besonderen Regeln jenseits normaler PHP-Klassen: Sie hat einen Konstruktor, typisierte Properties und kann sogar eigene Methoden besitzen. Üblicherweise ist eine Attribut-Klasse aber bewusst simpel gehalten, als reiner Datencontainer ohne Geschäftslogik, denn die eigentliche Verarbeitung findet später über Reflection statt, nicht im Attribut selbst.
<?php
declare(strict_types=1);
#[Attribute]
final class Route
{
// Plain data container: no business logic inside the attribute itself
public function __construct(
public readonly string $path,
public readonly string $method = 'GET',
) {
}
}
final class OrderController
{
#[Route('/api/orders', method: 'POST')]
public function create(): void
{
// Handler logic
}
#[Route('/api/orders/{id}')]
public function show(): void
{
// Handler logic
}
}
3. Ziel einschränken mit Attribute::TARGET und Flags kombinieren
Ohne weitere Angabe darf ein Attribut auf jedem beliebigen Sprachelement stehen: Klasse, Methode, Property, Parameter, Klassenkonstante oder Funktion. Das ist selten gewünscht, denn ein Routing-Attribut ergibt auf einer Property keinen Sinn. Die Konstante Attribute::TARGET_METHOD und ihre Geschwister TARGET_CLASS, TARGET_PROPERTY, TARGET_PARAMETER, TARGET_CLASS_CONSTANT und TARGET_FUNCTION schränken das erlaubte Ziel eines eigenen PHP-Attributs präzise ein.
Mehrere Ziele lassen sich mit dem bitweisen Oder-Operator kombinieren, etwa Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION, wenn ein Attribut sowohl auf Methoden als auch auf freien Funktionen zulässig sein soll. Wird ein Attribut versehentlich an einer nicht erlaubten Stelle verwendet, wirft PHP beim Instanziieren über Reflection einen Error, nicht schon beim Parsen der Datei, was bei der Fehlersuche zu beachten ist.
Der zweite Parameter des #[Attribute]-Konstruktors selbst ist eine Bitmaske aus TARGET-Konstanten. Fehlt die Angabe, gilt implizit Attribute::TARGET_ALL, also jedes mögliche Ziel. Für eigene PHP-Attribute, die in größeren Teams eingesetzt werden, ist eine explizite Zieleinschränkung fast immer sinnvoll, weil sie Missbrauch bereits strukturell verhindert, statt nur in der Dokumentation zu stehen.
<?php
declare(strict_types=1);
// Restricting the target prevents misuse on the wrong language element
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
final class Route
{
public function __construct(
public readonly string $path,
public readonly string $method = 'GET',
) {
}
}
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final class Sensitive
{
public function __construct(
public readonly string $reason = 'PII',
) {
}
}
final class Customer
{
public function __construct(
#[Sensitive('contains payment data')]
public readonly string $iban,
) {
}
}
4. PHP-Attribute per Reflection auslesen
Ein Attribut ohne Auswertung bleibt reine Deklaration ohne Effekt. Der zentrale Mechanismus, um eigene PHP-Attribute tatsächlich nutzbar zu machen, ist die Reflection-API. ReflectionMethod::getAttributes(), ReflectionClass::getAttributes() und ReflectionProperty::getAttributes() liefern jeweils ein Array von ReflectionAttribute-Objekten für das jeweilige Sprachelement.
Jedes ReflectionAttribute-Objekt bietet getName() für den vollqualifizierten Klassennamen des Attributs, getArguments() für die rohen, unverarbeiteten Konstruktor-Argumente, und, am wichtigsten, newInstance(), das eine echte Instanz der Attribut-Klasse erzeugt, inklusive vollständiger Typprüfung durch den regulären Konstruktor. Diese Instanziierung ist der Punkt, an dem ein Attribut von reinen Metadaten zu einem verwendbaren Objekt wird.
<?php
declare(strict_types=1);
final class Router
{
// Scans a controller class and builds a route table from #[Route] attributes
public function collectRoutes(string $controllerClass): array
{
$reflectionClass = new ReflectionClass($controllerClass);
$routes = [];
foreach ($reflectionClass->getMethods() as $method) {
foreach ($method->getAttributes(Route::class) as $attribute) {
/** @var Route $route */
$route = $attribute->newInstance();
$routes[] = [
'path' => $route->path,
'method' => $route->method,
'handler' => [$controllerClass, $method->getName()],
];
}
}
return $routes;
}
}
$router = new Router();
$routes = $router->collectRoutes(OrderController::class);
print_r($routes);
5. Konstruktor-Argumente, benannte Parameter und Default-Werte
Attribute nutzen dieselbe Konstruktoraufruf-Syntax wie gewöhnliche Objektinstanziierung, inklusive benannter Argumente. #[Route('/api/orders', method: 'POST')] zeigt, wie sich positionelle und benannte Argumente mischen lassen, genau wie bei einem normalen new Route(...)-Aufruf. Diese Konsistenz ist einer der Gründe, warum sich eigene PHP-Attribute so natürlich in bestehenden PHP-Code einfügen: Es gibt keine separate Syntax zum Lernen.
Default-Werte im Konstruktor der Attribut-Klasse funktionieren ebenfalls unverändert. Ein Parameter string $method = 'GET' erlaubt, das Attribut ohne diesen Parameter zu verwenden, sofern der Standardwert passt. Auch Enums als Konstruktor-Parameter sind seit PHP 8.1 als Attribut-Argumente erlaubt, was gegenüber einem rohen String zusätzliche Typsicherheit bringt: #[Route('/api/orders', method: HttpMethod::Post)] lässt ungültige Methodenbezeichner gar nicht erst zu.
6. Ein eigenes Validierungssystem auf Basis von PHP-Attributen bauen
Ein besonders anschauliches Praxisbeispiel für eigene PHP-Attribute ist ein einfaches Validierungssystem für Datentransferobjekte. Statt Validierungsregeln in einer separaten Konfigurationsdatei zu pflegen, stehen sie direkt über der jeweiligen Property, als #[NotBlank] oder #[Length(min: 3, max: 50)]. Ein generischer Validator liest per Reflection alle Properties einer Klasseninstanz aus, prüft deren Attribute und sammelt Verstöße in einer Fehlerliste.
Diese Architektur trennt sauber zwischen Deklaration und Ausführung: Die Attribut-Klassen wie NotBlank und Length definieren nur, welche Regel gilt und mit welchen Parametern, während eine separate ValidationRule-Schnittstelle mit einer validate(mixed $value): bool-Methode die eigentliche Prüfung implementiert. So bleibt jede Regel unabhängig testbar, ohne den Validator selbst anfassen zu müssen.
<?php
declare(strict_types=1);
interface ValidationRule
{
public function validate(mixed $value): bool;
public function message(string $property): string;
}
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
final class NotBlank implements ValidationRule
{
public function validate(mixed $value): bool
{
return is_string($value) && trim($value) !== '';
}
public function message(string $property): string
{
return "{$property} must not be blank.";
}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Length implements ValidationRule
{
public function __construct(
private readonly int $min,
private readonly int $max,
) {
}
public function validate(mixed $value): bool
{
$length = is_string($value) ? mb_strlen($value) : 0;
return $length >= $this->min && $length <= $this->max;
}
public function message(string $property): string
{
return "{$property} must be between {$this->min} and {$this->max} characters.";
}
}
final class Validator
{
// Reads validation rules from attributes and collects violations
public function validate(object $target): array
{
$errors = [];
$reflectionClass = new ReflectionClass($target);
foreach ($reflectionClass->getProperties() as $property) {
$value = $property->getValue($target);
foreach ($property->getAttributes(ValidationRule::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
/** @var ValidationRule $rule */
$rule = $attribute->newInstance();
if (!$rule->validate($value)) {
$errors[] = $rule->message($property->getName());
}
}
}
return $errors;
}
}
7. Mehrfach-Attribute mit IS_REPEATABLE verarbeiten
Standardmäßig darf ein Attribut nur einmal auf demselben Sprachelement stehen. Der zweite Versuch, dasselbe Attribut ein zweites Mal an derselben Stelle zu setzen, führt zu einem Fehler. Für Fälle, in denen mehrere unabhängige Regeln desselben Typs gelten sollen, etwa mehrere Berechtigungen oder mehrere Validierungsbedingungen, erlaubt die Flag Attribute::IS_REPEATABLE, dasselbe eigene PHP-Attribut beliebig oft zu wiederholen.
Beim Auslesen ändert sich dadurch nichts an der grundsätzlichen API: getAttributes() liefert ohnehin immer ein Array, unabhängig davon, ob das Attribut wiederholbar ist. Der Unterschied liegt allein darin, dass ohne IS_REPEATABLE höchstens ein Element in diesem Array vorkommen kann, während mit der Flag beliebig viele Vorkommen möglich sind, jedes mit potenziell unterschiedlichen Konstruktor-Argumenten.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class RequiresPermission
{
public function __construct(
public readonly string $permission,
) {
}
}
final class InvoiceController
{
// Repeated attribute: each occurrence is a separate ReflectionAttribute
#[RequiresPermission('invoice.view')]
#[RequiresPermission('invoice.export')]
public function export(): void
{
// Handler logic
}
}
$method = new ReflectionMethod(InvoiceController::class, 'export');
$permissions = array_map(
static fn (ReflectionAttribute $attribute): string => $attribute->newInstance()->permission,
$method->getAttributes(RequiresPermission::class),
);
print_r($permissions); // ['invoice.view', 'invoice.export']
8. PHP-Attribute vs. Interfaces und Docblocks: wann was
Nicht jede Metainformation gehört in ein Attribut. Wenn Verhalten zur Kompilierzeit über den Typechecker erzwungen werden soll, etwa dass eine Klasse zwingend eine bestimmte Methode implementiert, ist ein Interface die richtige Wahl, denn ein Attribut erzwingt nichts, es beschreibt nur. Eigene PHP-Attribute eignen sich dagegen für optionale, deklarative Zusatzinformationen, die eine externe Komponente wie ein Router oder Validator zur Laufzeit auswertet.
Docblocks bleiben weiterhin sinnvoll für reine Dokumentationszwecke ohne maschinelle Auswertung, etwa erklärende Beschreibungen oder @see-Verweise, sowie für PHPStan-spezifische Typannotationen wie @var array<int, string>, die es im nativen Typsystem noch nicht gibt. Sobald jedoch echte Laufzeitlogik von den Metadaten abhängt, etwa ein Router, der tatsächliche Pfade registriert, ist ein Attribut dem Docblock strukturell überlegen, weil es geparst und typgeprüft wird, statt als Freitext-Kommentar zu existieren.
| Aufgabe | Unsicher / umständlich | Empfohlen mit PHP-Attributen | Vorteil |
|---|---|---|---|
| Metadaten an Methode anhängen | PHPDoc-Kommentar wie @Route(...) | #[Route(...)] |
Vom Compiler geparst, kein Regex nötig |
| Mehrere Regeln an einer Property | Mehrere @Annotation-Zeilen im Kommentar | Attribut mit IS_REPEATABLE mehrfach setzen | Nativ, keine externe Library nötig |
| Attribut-Daten auslesen | Eigener Docblock-Parser mit Regex | getAttributes() per Reflection |
Robust, IDE-unterstützt, kein Custom-Parser |
| Ziel eingrenzen | Konvention oder Kommentar in der Doku | Attribute::TARGET_METHOD-Flag |
Vom Compiler durchgesetzt, nicht nur dokumentiert |
| Instanz aus Attribut erzeugen | Manuelles String-Parsing der Argumente | $attribute->newInstance() |
Typsicher, nutzt den regulären Konstruktor |
9. Performance-Overhead von Reflection und Caching-Strategien
Reflection ist nicht kostenlos: Jeder Aufruf von getAttributes() und jede Instanziierung über newInstance() kostet messbare Zeit, deutlich mehr als ein direkter Methodenaufruf. Für eigene PHP-Attribute, die bei jedem einzelnen Request neu ausgelesen werden, etwa bei jedem Routing-Vorgang, summiert sich dieser Overhead in produktiven Anwendungen mit hoher Anfragefrequenz spürbar auf.
Die übliche Lösung ist Caching der ausgewerteten Attribut-Daten, nicht der Reflection-Objekte selbst. Ein Router baut die Routentabelle aus allen Controllern typischerweise einmal beim Anwendungsstart oder Deployment auf und serialisiert das Ergebnis in eine Cache-Datei, etwa als PHP-Array oder als serialisiertes Objekt. Zur Laufzeit jeder einzelnen Anfrage wird dann nur noch dieser Cache gelesen, Reflection läuft nur einmal während des Build-Schritts, nie pro Request.
Für Entwicklungsumgebungen ist dieser Cache dagegen meist unerwünscht, weil Änderungen am Code sofort ohne manuellen Cache-Clear sichtbar sein sollen. Ein typisches Muster ist daher, den Attribut-Cache nur zu aktivieren, wenn eine Umgebungsvariable wie APP_ENV=production gesetzt ist, und in der Entwicklung stattdessen bei jeder Anfrage frisch per Reflection zu lesen.
10. Zusammenfassung
Eigene PHP-Attribute ersetzen fragile Docblock-Annotationen durch eine vom Compiler geparste, typsichere Syntax. Eine Klasse wird selbst mit #[Attribute] markiert, optional mit TARGET-Flags auf ein bestimmtes Sprachelement eingeschränkt und mit IS_REPEATABLE für Mehrfachverwendung geöffnet. Reflection-Methoden wie getAttributes() und newInstance() lesen die Daten zur Laufzeit aus und erzeugen echte, typgeprüfte Instanzen der Attribut-Klasse.
Attribute eignen sich für optionale, deklarative Metadaten, die eine externe Komponente auswertet, während Interfaces weiterhin die richtige Wahl für erzwungenes Verhalten bleiben. Da jede Reflection-Auswertung Laufzeitkosten verursacht, lohnt sich in produktiven Umgebungen mit hoher Anfragefrequenz ein Cache der ausgewerteten Attribut-Daten, aufgebaut einmalig beim Deployment statt bei jeder Anfrage neu.
Eigene PHP-Attribute und Reflection - Das Wichtigste auf einen Blick
Attribut-Klasse definieren
#[Attribute] auf einer normalen Klasse. Konstruktor-Argumente wie bei jeder anderen Klasse, meist als reiner Datencontainer.
Ziel einschränken
Attribute::TARGET_METHOD, TARGET_PROPERTY und weitere Flags, kombinierbar mit dem bitweisen Oder-Operator.
Per Reflection auslesen
getAttributes() liefert ReflectionAttribute-Objekte, newInstance() erzeugt eine typgeprüfte Instanz daraus.
Performance
Reflection kostet Laufzeit. Ausgewertete Attribut-Daten in Produktion cachen, in Entwicklung frisch auslesen.
11. FAQ: Eigene PHP-Attribute definieren und auswerten
1Können PHP-Attribute eigene Logik enthalten?
2Werden Attribute automatisch ausgewertet?
3Unterschied zu Docblock-Annotationen?
4Attribut mehrfach an einer Stelle nutzen?
5Welche TARGET-Flags gibt es?
6newInstance() vs. getArguments()?
7Können promoted Properties Attribute bekommen?
8Enums als Attribut-Argument möglich?
9Wie teuer ist Reflection zur Laufzeit?
10Interface statt Attribut verwenden?
Mironsoft
PHP-Architektur, Codequalität und Magento-Entwicklung
Eigene PHP-Attribute sauber im eigenen Projekt einführen?
Wir konzipieren eigene PHP-Attribute für Routing, Validierung und Zugriffskontrolle, inklusive Reflection-Auswertung mit Caching-Strategie für produktive Umgebungen und vollständiger PHPStan-Absicherung.
Attribut-Design
Eigene Attribut-Klassen mit passenden TARGET-Flags konzipieren
Reflection-Systeme
Validierung, Routing oder Serialisierung auf Attribut-Basis implementieren
Performance-Tuning
Caching-Strategien für Reflection-lastige Attribut-Auswertung in Produktion