Ein Blick auf PHP 8: Teil 9

PHP 8.0-Features im Fokus: Attribute
Keine Kommentare

PHP 8 wurde veröffentlicht und mit ihm eine Reihe von Verbesserungen, neuen Funktionen und einer generellen Überarbeitung, um die weit verbreitete serverseitige Websprache noch besser zu machen. In dieser Serie wollen wir darauf eingehen, was Sie über die neue Version wissen sollten.

Letztes Mal haben wir uns eines der drei Hauptfeatures von PHP 8.0 angesehen: Constructor Property Promotion. Heute, im vorletzten Teil unserer Serie, schauen wir uns das zweite an: Attribute.

Eine Geschichte voller Veränderungen

Einige PHP-Änderungen werden in kurzer Zeit vorgeschlagen, diskutiert, implementiert und genehmigt. Sie sind unumstritten, populär und haben einen natürlichen Weg, sie zu implementieren. Und dann gibt es die, die ausprobiert werden, scheitern, und mehrmals wieder auftauchen, bevor sie schlussendlich akzeptiert werden. Manchmal dauert die Umsetzung lange, manchmal ist die Idee selbst unausgereift, und manchmal hat sich die Community einfach noch nicht für eine Idee begeistern können. Attribute fallen in die letzte Kategorie. Sie wurden zum ersten Mal im Jahr 2016 für PHP 7.1 vorgeschlagen, stießen aber auf hartnäckigen Widerstand und verloren die Abstimmung. Spulen Sie vier Jahre vor und ein sehr ähnlicher, wenn auch leicht abgespeckter Vorschlag, wurde mit nur einer Gegenstimme durchgewunken. Es ist offenbar eine Idee, deren Zeit jetzt tatsächlich gekommen ist.

Metamodellierung

Was sind also 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 Doctrine Annotations verwendet haben, sind Attribute im Wesentlichen genau das, allerdings als First Class Citizens in der Sprachsyntax. Die Begriffe „Attribute“ und „Annotationen“ werden in den verschiedenen Sprachen, die sie unterstützen, grob austauschbar verwendet. PHP hat sich für den Begriff „Attribut“ entschieden, um Verwechslungen mit Doctrine Annotations zu vermeiden, da diese eine unterschiedliche Syntax besitzen. 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 weit mehr Diskussionen als das Feature selbst. Dies sei an dieser Stelle allerdings übersprungen und wir werden uns lediglich dem Endergebnis widmen. 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
    {
        // ...
    }
}

Die verschiedenen #[…]-Blöcke sind Attribute. In der Laufzeit tun sie … absolut nichts. Sie haben keinen Einfluss auf den Code selbst, stehen jedoch dem Reflection-API zur Verfügung, die es anderem Code erlaubt, die Klasse Product oder ihre Eigenschaften und Methoden zu untersuchen und zusätzliche Aktionen auszuführen. Welche Aktion genau ist, das bleibt dem Nutzer überlassen.

Als Nebeneffekt werden Attribute in älteren PHP-Versionen als Kommentare interpretiert und somit ignoriert, wenn sie einzeilig sind. Das ist eher ein nützlicher Nebeneffekt als ein gewolltes Feature, aber dennoch erwähnenswert. Es wird wahrscheinlich auch in Ihrem Browser als Kommentar gerendert. Es wird noch eine Weile dauern, bis Syntax-Highlighter und IDEs aufholen, aber das ist nur eine Frage der Zeit.
Ein wichtiger Hinweis zu Attributen ist, dass sie nicht auf Strings beschränkt sind. Tatsächlich werden sie in der überwältigenden Mehrheit der Fälle Objekte sein, die Parameter annehmen können.

Attribute können an Klassen, Eigenschaften, Methoden, Funktionen, Klassenkonstanten und sogar an Funktions-/Methodenparameter angehängt werden. Das sind noch mehr Einsatzmöglichkeiten als Doctrine-Annotationen.

International PHP Conference

Frameworkless – the new black?

by Carsten Windler (KW-Commerce GmbH)

Getting started with PHP-FFI

by Thomas Bley (Bringmeister GmbH)

JSON-Schema von 0 auf 100

by Ingo Walther (Electronic Minds GmbH)

IT Security Camp 2021

Interview mit Christian Schneider zum Thema „DevSecOps“

DevSecOps ist, bezogen auf Security-Checks, die logische Fortführung der Automatisierung im DevOps-Sinne

IT-Security Experte, Christian Schneider

Christian ist als freiberuflicher Whitehat Hacker, Trainer und Security-Coach tätig. Als Softwareentwickler mit mittlerweile 20 Jahren Erfahrung, fand er 2005 seinen Themenschwerpunkt im Bereich IT-Security.

Ein praktisches Beispiel

All dies scheint ziemlich abstrakt, also sehen wir uns an dieser Stelle ein praktisches Beispiel an. Die meisten Verwendungen von Attributen/Annotations 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 Event-System. Zu diesem Zweck habe ich meine PSR-14 Event Dispatcher-Implementierung, Tukio, aktualisiert, um Attribute zu unterstützen. Es funktioniert (etwas vereinfacht) folgendermaßen.
Tukio bietet einen Listener-Provider, der Listener, also jede Art von Callables, an die ein Event übergeben werden kann, aggregiert und anordnet. Normalerweise kann ein Listener wie folgt registriert werden:

<?php

function my_listener(MyEventType $event): void { ... }

$provider = new OrderedListenerProvider();

$provider->addListener('my_listener', 5, 'listener_a', MyEventType::class);

Diese Parameter sind das Callable, das hinzugefügt werden soll (in diesem Fall der Funktionsname) und optional eine Priorität für die Reihenfolge, eine benutzerdefinierte ID und der Typ des Ereignisses. Die letzten beiden werden in der Regel automatisch abgeleitet, hier werden sie eingefügt, um zu demonstrieren, dass man sie auch im laufenden Betrieb angeben kann. Es gibt auch Methoden, um Listener vor oder nach einem anderen Listener hinzuzufügen, basierend auf dessen ID. Es ist allerdings mühsam, alle diese Methoden spontan zu spezifizieren. Besonders schön wäre es, wenn man die ID zusammen mit dem Listener angeben könnte, damit es konsistent ist. Genau das machen Attribute möglich. Zuerst definieren wir unser neues Attribut. Attribute sind eine normale PHP-Klasse, die selbst ein spezielles Attribut haben:

<?php

namespace Crell\Tukio;

use \Attribute;

#[Attribute]
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.“ Beachtet auch, dass, da das Attribut selbst eine Klasse ist, den Namespace-Regeln unterliegt und am Anfang der Datei geused werden muss. Die Klasse definiert dann auch einen Constructor unter Verwendung der neuen Constructor-Promotion-Syntax. Dies ist eine rein interne Datenklasse, also geben wir ihr nur zwei optionale öffentliche Eigenschaften, die vom Constructor ausgefüllt werden. (In PHP 7.4 wäre der gleiche Code doppelt so lang.)

Gehen wir noch einen Schritt weiter: Es ergibt keinen Sinn, ein Listener-Attribut auf einen Parameter oder eine Klasse zu legen. Sinnvoll ist dies nur bei 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 auf eine Nicht-Funktion oder Nicht-Methode zu legen, wird es einen Fehler werfen, wenn es verwendet werden soll. Das deutet auch darauf hin, wie wir das Listener-Attribut zur Übergabe von Parametern verwenden werden.

<?php

use Crell\Tukio\Listener;

#[Listener('listener_a')]
function my_listener(MyEventType $event): void { ... }

$provider = new OrderedListenerProvider();

$provider->addListener('my_listener', 5);

Für sich genommen macht das nichts. Aber sobald wir den Namen der Funktion haben, können wir die Reflection-API verwenden, um diese Informationen einzuholen. Eine (sehr) vereinfachte Version von dem, was Tukio hier macht, ist:

<?php

Use Crell\Tukio\Listener;

/// A string means it's a function name, so reflect on it as a function.
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 Beispiel ist $attribs ein Array von \ReflectionAttribute-Objekten. Es gibt ein paar Dinge, die man damit machen kann, aber vor allem muss das Attribut nicht unbedingt eine Klasse sein! Wir können z. B. 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 pure Existenz uns bereits alles sagt, was wir wissen müssen.

Die Methode getAttributes() kann auch die Attribute für einen Wert filtern. In diesem Fall beschränken wir uns darauf, nur Listener-Attribute zurückzugeben und Unterklassen von Listener einzubeziehen. Obwohl dies optional ist, empfehle ich dringend, beides zu tun, da so Attribute aus anderen Bibliotheken herausgefiltert werden, die eventuell so nicht zu erwartensind Das Zulassen von Unterklassen oder Implementierungen bedeutet, dass wir eine Reihe von Attributen über ein Interface zusammenfassen können.

Die letzte Zeile mappt über dieses Array und ruft newInstance() für jedes Attribut auf. Das ruft direkt den Listener-Constructor mit den angegebenen Parametern auf und gibt das Ergebnis zurück. Das Objekt, das zurückkommt, ist identisch mit dem, was Sie erhalten würden, wenn Sie einfach new Listener('listener_a') geschrieben hätten. Über diesen Constructor-Aufruf hinaus kann die Attributklasse alles tun oder haben, was jede andere Klasse auch. Sie können ihr eine komplexe interne Logik geben oder auch nicht, Vorgabewerte behandeln, bestimmte Parameter erforderlich machen usw.

Nun kann Tukio die Daten aus dem addListener()-Methodenaufruf und dem Listener-Attribut kombinieren, wie es möchte. In diesem Fall ist der Effekt derselbe, als hätten Sie die ID im Methodenaufruf und nicht im Attribut angegeben. Ein einzelnes Language Item kann mit mehreren Attributen versehen werden, sogar mit Attributen aus völlig unterschiedlichen Bibliotheken. Attribute können sich auch auf ein einzelnes Language Item wiederholen lassen 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 der Kürze nicht eingehen kann. Aber das sollte bereits einen Vorgeschmack darauf geben, was möglich ist.

Demnächst in einem Framework in Ihrer Nähe

Die Unterstützung von Attributen taucht bereits in Frameworks auf. Das kommende Symfony 5.2 wird zum Beispiel Attribut-Versionen der Route Definition enthalten. Das bedeutet, dass Sie in der Lage sein werden, einen Controller zu deklarieren und ihn mit einer Route zu verdrahten:

<?php

use Symfony\Component\Routing\Annotation\Route;

class SomeController
{
    #[Route('/path', 'action_name')]
    public function someAction()
    {
        // ...
    }
}

Größtenteils wird dabei nur die Syntax der Doctrine-Annotationen gegen native Attribute ausgetauscht, um das gleiche Ziel zu erreichen. Es kann sogar gesteuert werden, welche Abhängigkeiten an die Controller-Parameter über Attribute an 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)
    {
        // ...
    }
}

Das sagt dem Argument Resolver, dass er den aktuellen Benutzer an den Parameter $user übergeben soll.
Zweifelsohne werden wir auch in anderen Frameworks und Bibliotheken mehr Verwendung von Attributen sehen, da es jetzt erstklassige native Unterstützung in der Sprache gibt.

Promoted Arguments

Es ist auch wichtig zu beachten, wie Attribute bei Promoteted Constructor Arguments funktionieren. In PHP 8.0 ist es jetzt möglich, eine Objekteigenschaft und ein Constructor-Argument gleichzeitig anzugeben, wenn das eine auf das andere abgebildet wird. So wie in diesem Beispiel:

<?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 ergibt nicht immer Sinn, das eine oder das andere zu verwenden. Die Engine kann nicht wissen, für was sie sich entscheiden soll. Für 8.0 wurde entschieden, 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 ergibt, filtern Sie es entweder in Ihrer eigenen Anwendungslogik heraus oder verwenden Sie keine Constructor-Promotion für diesen einen Parameter, so dass Sie das Attribut nur an einer Stelle vergeben können und nicht an der anderen.

Native Attributes

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 das so bleibt. Der RFC nennt mehrere mögliche zukünftige Attribute, die für die Engine in Zukunft eine Bedeutung haben 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 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 mitteilen könnte, 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 es sind diese Ideen, die Engine-Entwicklern für eine Verbesserung von zukünftigen Versionen aufnehmen können.

Eine lange Liste von Credits

Die Attribute wurden im Laufe von mehreren RFCs entwickelt und modifiziert. Der ursprüngliche RFC stammt von Benjamin Eberlei und Martin Schröder und verwendete eine andere Syntax, die von älteren Versionen von Hack entlehnt wurde. Ein Folge-RFC von denselben Leuten hat 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.

Ausblick

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 geht mit diesem Attribut vor sich? Nächste Woche werden wir es aufklären.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Abonnieren
Benachrichtige mich bei
guest
0 Comments
Inline Feedbacks
View all comments
X
- Gib Deinen Standort ein -
- or -