Manche PHP-Änderungen werden in kurzer Zeit vorgeschlagen, diskutiert, implementiert und genehmigt. Sie sind unumstritten, populär und lassen sich auf natürliche Weise implementieren.
Und dann gibt es die, die ausprobiert werden, scheitern und mehrfach wieder aufgegriffen werden, bevor sie schließlich akzeptiert werden. Manchmal dauert es lange, bis die Umsetzung gelingt, manchmal ist die Idee selbst nur unausgereift, und manchmal hat sich die Gemeinschaft einfach noch nicht für eine Idee erwärmt - "es ist noch nicht an der Zeit".
Attribute fallen genau in die letzte Kategorie. Sie wurden erstmals 2016 für PHP 7.1vorgeschlagen, stießen aber auf hartnäckigen Widerstand und verloren die Abstimmung über die Akzeptanz. Vier Jahre später wurde ein sehr ähnlicher Vorschlag, wenn auch mit etwas geringerem Umfang, mit nur einer Gegenstimme angenommen. Offenbar ist die Zeit für diese Idee tatsächlich reif.
Was also sind Attribute? Attribute sind deklarative Metadaten, die an andere Teile der Sprache angehängt werden können und dann zur Laufzeit analysiert werden, um das Verhalten zu steuern.
Wenn Sie schon einmal mit Doctrine Annotationsgearbeitet haben , sind Attribute im Grunde genommen genau das, allerdings als Bürger erster Klasse in der Sprachsyntax. Die Begriffe "Attribute" und "Annotationen" werden in den verschiedenen Sprachen, die sie unterstützen, in etwa austauschbar verwendet. PHP hat sich für den Begriff "Attribut" entschieden, um Verwechslungen mit Doctrine Annotations zu vermeiden, da diese eine andere Syntax haben. Da Attribute jedoch in die Sprache eingebaut sind und nicht nur eine Dokumentationskonvention im User-Space sind, können sie von statischen Analysatoren, IDEs und Syntax-Highlightern gelintet und typgeprüft werden.
Die spezifische Syntax war Gegenstand von weitaus mehr Diskussionen als das Feature selbst, aber ich werde das alles überspringen und nur das Endergebnis anhand eines Beispiels von PHP-Attributen betrachten. Attribute in PHP 8.0 haben ihre Syntax von Rust übernommen:
<?php #[GreedyLoad] class Product { #[Positive] protected int $id; #[Admin] public function refillStock(int $quantity): bool { // ... } }
Diese verschiedenen #[...] Blöcke sind Attribute. Zur Laufzeit tun sie ... absolut nichts. Sie haben keinen Einfluss auf den Code selbst. Sie stehen jedoch der Reflection-API zur Verfügung, die es anderem Code ermöglicht, die KlasseProduct oder ihre Eigenschaften und Methodenzu untersuchen und zusätzliche Maßnahmen zu ergreifen. Welche Aktion das ist, bleibt Ihnen überlassen, denn Sie können sie an Ihre Bedürfnisse anpassen.
Als Nebeneffekt werden Attribute in älteren PHP-Versionen als Kommentare interpretiert und daher ignoriert, wenn sie einzeilig sind. Das ist eher ein nützlicher Nebeneffekt als ein beabsichtigtes Feature, aber es ist erwähnenswert. Es wird wahrscheinlich auch in Ihrem Browser als Kommentar dargestellt. Es wird noch eine Weile dauern, bis Syntax-Highlighter und IDEs aufholen, aber das wird schon bald der Fall sein.
Ein wichtiger Hinweis zu Attributen ist, dass sie nicht auf Zeichenketten beschränkt sind. In der überwältigenden Mehrheit der Fälle werden sie Objekte sein, die Parameter annehmen können.
Benutzerdefinierte PHP-Attribute können an Klassen, Eigenschaften, Methoden, Funktionen, Klassenkonstanten und sogar an Funktions-/Methodenparameter angehängt werden. Das sind sogar noch mehr Stellen als Doctrine-Annotationen.
All dies scheint ziemlich abstrakt, also lassen Sie uns ein praktisches Beispiel für PHP-Attribute nehmen. Die meisten Anwendungsfälle von Attributen/Anmerkungen in PHP drehen sich heute um die Registrierung. Das heißt, sie sind ein deklarativer Weg, um einem System Details über ein anderes System mitzuteilen, wie zum Beispiel ein Plugin oder ein Ereignissystem. Zu diesem Zweck habe ich meine PSR-14 Event Dispatcher-Implementierung, Tukio,aktualisiert , um Attribute zu unterstützen. Es funktioniert folgendermaßen (etwas vereinfacht).
Tukio bietet einen "Listener-Provider", der "Listener" zusammenfasst und anordnet, d. h. jede Art von Callable, an die ein Ereignis übergeben werden kann. Normalerweise können Sie einen Listener wie folgt registrieren:
<?php function my_listener(MyEventType $event): void { ... } $provider = new OrderedListenerProvider(); $provider->addListener('my_listener', 5, 'listener_a', MyEventType::class);
Diese Parameter sind die aufzurufende Funktion (in diesem Fall der Funktionsname) und optional eine Priorität für die Reihenfolge, eine benutzerdefinierte ID und der Typ des Ereignisses, auf das man hören will. Die beiden letztgenannten Parameter werden in der Regel automatisch abgeleitet, aber ich füge sie hier ein, um zu zeigen, dass man sie auch im laufenden Betrieb angeben kann. Es gibt auch Methoden zum Hinzufügen von Listenern vor oder nach einem anderen Listener, basierend auf dessen ID.
Es ist allerdings sehr mühsam, all dies spontan zu spezifizieren. Besonders schön wäre es, wenn man die ID zusammen mit dem Listener angeben könnte, damit sie konsistent ist. Mit Attributen können wir das tun.
Zuerst definieren wir unser neues Attribut. Attribute sind eine normale PHP-Klasse, die ihrerseits ein spezielles Attribut haben:
<?php namespace Crell\Tukio; use \Attribute; #[Attribut] class Listener implements ListenerAttribute { public function __construct( public ?string $id = null, public ?string $type = null, ) {} }
Das Attribut #[Attribut] sagt PHP: "Ja, diese Klasse kann als Attribut geladen werden". Da das Attribut selbst eine Klasse ist, unterliegt es den Namespace-Regeln und muss am Anfang der Datei verwendet werden.
Die Klasse definiert dann auch einen Konstruktor ... unter Verwendung der neuen Konstruktor-Promotion-Syntax. Dies ist eine rein interne Datenklasse, also geben wir ihr nur zwei optionale öffentliche Eigenschaften, die vom Konstruktor gefüllt werden. (In PHP 7.4 wäre derselbe Code doppelt so lang.)
Gehen wir noch einen Schritt weiter: Es macht keinen Sinn, ein Listener-Attribut auf einen Parameter oder eine Klasse zulegen , nur auf Funktionen und Methoden. Daher können wir der Klasse mit Hilfe einer Reihe von Bit-Flags mitteilen, dass es sich um ein gültiges Attribut nur für Funktionen und Methoden handelt:
<?php namespace Crell\Tukio; use \Attribute; #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)] class Listener implements ListenerAttribute { public function __construct( public ?string $id = null, public ?string $type = null, ) {} }
Wenn wir nun versuchen, ein Listener-Attribut an eine Nicht-Funktion oder Nicht-Methode anzuhängen, wird es einen Fehler auslösen, wenn wir versuchen, es zu verwenden.
Das deutet auch darauf hin, wie wir das Listener-Attribut zur Übergabe von Parameternverwenden werden .
<?php use Crell\Tukio\Listener; #[Listener('listener_a')] function my_listener(MyEventType $event): void { ... } $provider = new OrderedListenerProvider(); $provider->addListener('my_listener', 5);
Das allein bewirkt noch nichts. Aber wenn wir den Namen der Funktion kennen, können wir die Reflection-API verwenden, um diese Informationen herauszuholen. Eine (sehr) vereinfachte Version dessen, was Tukio hier tut, ist:
<?php Use Crell\Tukio\Listener; /// Ein String bedeutet, dass es sich um einen Funktionsnamen handelt, also reflektieren Sie ihn als Funktion.
if (is_string($listener)) { $ref = new \ReflectionFunction($listener); $attribs = $ref->getAttributes(Listener::class, \ReflectionAttribute::IS_INSTANCEOF); $attributes = array_map(fn(\ReflectionAttribute $attrib) => $attrib->newInstance(), $attribs); }
In diesem PHP-Attribut-Beispiel ist $attribs ein Array von \ReflectionAttribute-Objekten. Es gibt ein paar Dinge, die man mit einem dieser Objekte tun kann. Insbesondere muss das Attribut nicht unbedingt eine Klasse sein! Sie können nur den Namen und die Argumente (falls vorhanden) als String bzw. Array erhalten. Das kann für die statische Analyse oder für einfache Flag-Attribute hilfreich sein, deren Vorhandensein Ihnen bereits alles sagt, was Sie wissen müssen.
Die Methode getAttributes() kann die Attribute auch nach einem Wert filtern. In diesem Fall beschränken wir sie darauf, nur Listener-Attribute zurückzugeben und auch Unterklassen von Listener einzubeziehen. Obwohl dies optional ist, empfehle ich dringend, beides zu tun, da es auf natürliche Weise Attribute aus anderen Bibliotheken herausfiltert, die Sie vielleicht nicht erwarten, und das Zulassen von Unterklassen oder Implementierungen bedeutet, dass Sie eine Reihe von Attributen mithilfe einer Schnittstelle zusammenfassen können.
Die letzte Zeile bildet dieses Array ab und ruft newInstance() für jedes Attribut auf. Dies ruft direkt den Listener-Konstruktor mit den angegebenen Parametern auf und gibt das Ergebnis zurück. Das zurückgegebene Objekt ist identisch mit dem, das Sie erhalten würden, wenn Sie einfach new Listener('listener_a') geschrieben hätten . Über diesen Konstruktoraufruf hinaus kann die Attributklasse alles tun oder haben, was jede andere Klasse auch tun oder haben kann. Sie können ihr eine komplexe interne Logik geben oder auch nicht, mit Standardwerten umgehen, bestimmte Parameter erforderlich machen usw., wie es Ihre Situation erfordert.
Nun kann Tukio die Daten aus dem addListener() -Methodenaufruf und dem Listener-Attribut beliebig kombinieren. In diesem Fall ist der Effekt derselbe, als hätten Sie die ID im Methodenaufruf und nicht im Attribut angegeben.
Ein einzelnes Sprachelement kann mit mehreren Attributen versehen werden, sogar mit Attributen aus völlig unterschiedlichen Bibliotheken. Attribute können sich auch in einem einzelnen Sprachelement wiederholen oder nicht. Manchmal kann es sinnvoll sein, dasselbe Attribut zweimal zuzulassen, ein anderes Mal nicht. Beides kann erzwungen werden.
Es gibt noch viel mehr, was Sie tun können, und Tukio hat tatsächlich mehrere Attribute, die es für verschiedene Anwendungsfälle und Situationen unterstützt, auf die ich hier aus Gründen der Kürze nicht eingehen werde, aber das sollte Ihnen einen Vorgeschmack darauf geben, was möglich ist.
Die Unterstützung von Attributen ist bereits in Frameworks zu finden. Das kommende Symfony 5.2 wird zum BeispielAttributversionen der Routendefinitionenthalten . Das bedeutet, dass Sie in der Lage sein werden, einen Controller zu deklarieren und ihn mit einer Route wie folgt zu verbinden:
<?php use Symfony\Component\Routing\Annotation\Route; class SomeController { #[Route('/path', 'action_name')] public function someAction() { // ... } }
Im Großen und Ganzen wird hier nur die Syntax der Doctrine-Annotationen gegen native Attribute ausgetauscht, um das gleiche Ziel zu erreichen. Aber es geht noch einen Schritt weiter. Sie können sogar steuern, welche Abhängigkeiten an Ihre Controller-Parameter über Attribute auf den Parameternübergeben werden , etwas, das Doctrine nicht konnte.
<?php namespace App\Controller; use App\Entity\MyUser; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Security\Http\Attribute\CurrentUser; class SomeController extends AbstractController { public function index(#[CurrentUser] MyUser $user) { // ... } }
Damit werden die Argumentauflöser angewiesen, den aktuellen Benutzer an den Parameter$userzu übergeben .
Zweifellos werden wir auch in anderen Frameworks und Bibliotheken mehr Verwendung von Attributen sehen, jetzt, da die Sprache eine erstklassige native Unterstützung bietet.
Es ist auch wichtig zu wissen, wie Attribute bei promoteten Konstruktorargumenten funktionieren. Erinnern Sie sich daran, dass es in PHP 8.0 nun möglich ist, eine Objekteigenschaft und ein Konstruktorargument gleichzeitig zu spezifizieren, wenn das eine auf das andere abgebildet wird, so wie hier:
<?php class Point { public function __construct(public int $x, public int $y) {} }
Sowohl Argumente als auch Eigenschaften können jedoch Attribute haben. Bezieht sich also in diesem Beispiel das Attribut auf das Argument oder die Eigenschaft?
<?php class Point { public function __construct( #[Positive] public int $x, #[Positive] public int $y, ) {} }
Technisch gesehen könnte es beides sein, aber es macht nicht immer Sinn für das eine oder das andere. Die Engine kann nicht wissen, welches von beiden es sein soll.
Für 8.0 wurde beschlossen, beides zu tun. Das Attribut wird sowohl für die Eigenschaft als auch für das Argument verfügbar sein. Wenn das für Ihre Anwendung keinen Sinn macht, filtern Sie es entweder in Ihrer eigenen Anwendungslogik heraus oder verwenden Sie keine Konstruktor-Promotion für diesen einen Parameter, so dass Sie ihn nur an einer Stelle attribuieren können und nicht an der anderen.
In 8.0 gibt es keine Attribute, die für die PHP-Engine eine Bedeutung haben, abgesehen von der Klasse\Attribute selbst. Es ist jedoch unwahrscheinlich, dass dies so bleiben wird. Der RFC nennt mehrere mögliche zukünftige Attribute, die für die Engine in Zukunft von Bedeutung sein könnten.
Zum Beispiel könnte ein #[Memoize] -Attribut der Engine mitteilen, dass eine Funktion oder Methode sicher zu cachen ist, und die Engine könnte dann automatisch die Ergebnisse dieser Funktion cachen. Eine weitere Option wäre ein #[Jit] -Attribut, das der JIT-Engine Hinweise darauf geben könnte, dass eine bestimmte Funktion oder Klasse ein guter oder schlechter Kandidat für die JIT-Kompilierung ist, oder die Art und Weise, wie sie kompiliert wird, beeinflussen könnte. Vielleicht sogar ein #[Inline] -Attribut, das dem Compiler mitteilt, dass er versuchen soll, einen Funktionsaufruf zu inlinen, um so Zeit für den Aufruf selbst zu sparen.
Keines dieser Engine-Attribute existiert bisher, aber sie sind die Art von Ideen, die den Engine-Entwicklern jetzt zur Verfügung stehen, um die Sprache in zukünftigen Versionen weiter zu verbessern.
Die Attribute wurden im Laufe mehrerer RFCs entwickelt und geändert.
Der ursprüngliche RFC stammt von Benjamin Eberlei und Martin Schröder und verwendete eine andere Syntax, die von älteren Versionen von Hack übernommen wurde. In einem Folge-RFC von denselben Personen wurden die Funktionen ein wenig optimiert.
Die Syntax wurde danach zweimal geändert, in RFCs von Theodore Brown und Martin Schröder und dann von Derick Rethans und Benjamin Eberlei, um sich auf die endgültige Syntax zu einigen.
Nächste Woche werden wir das letzte der drei großen Features behandeln, die die PHP-Welt in 8.0 verändern werden. Als kleinen Vorgeschmack habe ich die obigen Symfony-Beispiele aus dem Ankündigungs-Blogpost modifiziert. Die tatsächliche Syntax, die sie zeigen, sieht wie folgt aus:
<?php class SomeController { #[Route('/path', name: 'action')] public function someAction() { // ... } }
Was hat es mit diesem Attribut auf sich? Bleiben Sie dran ...