Kompetenzen Portfolio Referenzen Lebenslauf Blog Termin buchen

Rector: Annotationen in PHP-Attribute umwandeln

Gepostet von: Felix Dziekan in: Blog am 

PHP 8 hat uns Attribute gebracht, und jedes Framework ist umgestiegen. Womit du eine Codebase voller Annotationen in Docblocks hast, eine Deprecation-Warnung pro Datei und null Lust, mehrere hundert Klassen von Hand umzustellen.

Rector macht das in einem Lauf. Hier ist das Setup, das tatsächlich funktioniert hat, inklusive des Teils, den die Tutorials auslassen.

Installieren und trocken laufen lassen

 

composer require rector/rector --dev

 

Dann — vor allem anderen — ein Trockenlauf:

 

vendor/bin/rector process --dry-run

 

Das druckt ein Diff jeder Änderung, die es machen würde, und fasst nichts an. Lies es. Rector ist ein Code-Umschreiber, der auf deinen gesamten Quellbaum zeigt, und die fünf Minuten, die du in das Diff steckst, stehen zwischen dir und einem sehr verwirrenden Vormittag.

Wenn du zufrieden bist:

 

vendor/bin/rector process

 

Committe vorher, klar. Nicht zur Sicherheit — wegen des Diffs, damit das Review dir exakt zeigt, was sich geändert hat.

Die Konfiguration

Rector braucht eine rector.php im Projektwurzelverzeichnis. Die Pfade sind wichtig: Zeig auf deinen eigenen Code, nie auf vendor.

 

withPaths([
        __DIR__ . '/config',
        __DIR__ . '/public',
        __DIR__ . '/src',
        __DIR__ . '/tests',
    ])
    ->withSets([
        \Rector\Doctrine\Set\DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
        \Rector\Symfony\Set\SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES,
        \Rector\Symfony\Set\SensiolabsSetList::ANNOTATIONS_TO_ATTRIBUTES,
        \Rector\Symfony\Set\JMSSetList::ANNOTATIONS_TO_ATTRIBUTES,
        \Rector\Symfony\Set\FOSRestSetList::ANNOTATIONS_TO_ATTRIBUTES,
    ]);

 

Nimm tests mit rein. Das ist das Verzeichnis, das die Leute weglassen, und dann nutzt die Hälfte deiner Testsuite weiter Annotationen, die das Framework nicht mehr liest.

Ein Set pro Library, die du nutzt. Wenn du neben Symfony und Doctrine auch FOSRestBundle und JMS Serializer einsetzt, brauchst du alle vier — das Symfony-Set weiß nichts über JMS-Annotationen und läuft schlicht daran vorbei.

Die Sets decken die großen Libraries ab; alles andere braucht ein explizites Mapping

Der Teil, den niemand erwähnt: Annotationen, die die Sets nicht kennen

Hier wird aus einer "ein Befehl"-Migration eine echte Aufgabe. Die offiziellen Sets decken Symfony, Doctrine und eine Handvoll großer Libraries ab. Sie decken nicht ab:

  • Annotationen kleinerer Drittanbieter-Pakete — in meinem Fall die OpenAPI-Annotationen aus zircote/swagger-php
  • Annotationen, die du selbst geschrieben hast, in deinen eigenen Bundles

Lass Rector laufen, ohne die zu behandeln, und du bekommst eine Codebase in zwei Zuständen: Framework-Annotationen umgestellt, alles andere weiter in Docblocks. Was schlimmer ist als gar nicht anzufangen, denn jetzt kann niemand mehr sagen, welche Konvention dieses Projekt nutzt.

Die Lösung ist AnnotationToAttributeRector, mit selbst konfiguriertem Mapping:

 

use Rector\Php80\Rector\Class_\AnnotationToAttributeRector;
use Rector\Php80\ValueObject\AnnotationToAttribute;

    ->withConfiguredRule(AnnotationToAttributeRector::class, [
        new AnnotationToAttribute('OpenApi\\Annotations\\Get', 'OpenApi\\Attributes\\Get'),
        new AnnotationToAttribute('OpenApi\\Annotations\\Post', 'OpenApi\\Attributes\\Post'),
        new AnnotationToAttribute('OpenApi\\Annotations\\Response', 'OpenApi\\Attributes\\Response'),
        new AnnotationToAttribute('OpenApi\\Annotations\\Schema', 'OpenApi\\Attributes\\Schema'),
        // …und deine eigenen
        new AnnotationToAttribute(
            'Serie3\\HelperBundle\\Annotations\\ExcludeFromClone',
            'Serie3\\HelperBundle\\Attributes\\ExcludeFromClone'
        ),
    ]);

 

Alter Klassenname, neuer Klassenname, eine Zeile pro Stück. Mühsam, aber es ist der Unterschied zwischen einer fertigen und einer halbfertigen Migration.

Zwei Dinge muss man wissen. Das schreibt nur die Verwendung um — wenn die Attribut-Klasse in der Library oder in deinem eigenen Bundle noch nicht existiert, musst du sie vorher anlegen, sonst schreibst du funktionierenden Code in Verweise auf eine Klasse um, die es nicht gibt. Und um zu finden, was dir fehlt, grep nach @ in Docblocks, nachdem der Lauf durch ist; was übrig bleibt, hat kein Set abgedeckt.

Das eine, was du danach prüfen musst

Annotationen waren Strings in Kommentaren. Attribute sind echtes PHP, aufgelöst zur Compile-Zeit. Jede Attribut-Klasse muss also importiert sein.

Rector ergänzt die use-Statements, und es ist gut darin — aber das ist die eine Fehlerklasse, nach der zu suchen sich gezielt lohnt, denn ein fehlender Import scheitert zur Laufzeit statt in deinem Editor. Leer den Cache und boote die Anwendung, bevor du die Migration als erledigt betrachtest. In Symfony scheitert ein falsches Route-Attribut erst, wenn der Routing-Cache neu gebaut wird.

Lass deine Testsuite vorher und nachher laufen, und vergleiche. Nicht "lass sie danach laufen" — vergleiche. Du willst wissen, ob ein Fehler neu ist, und ohne das Vorher gibt es keine Möglichkeit, das zu sagen.

Und dann den Annotation-Reader löschen

Sobald nichts mehr Annotationen nutzt:

 

composer remove doctrine/annotations

 

Wenn es als transitive Abhängigkeit zurückkommt, in Ordnung — aber nichts von dir sollte es verlangen. Es zu entfernen heißt auch, dass die Migration nicht still zurückfallen kann, denn die nächste Annotation, die jemand schreibt, funktioniert einfach nicht.

Hat es sich gelohnt?

Ja, und zwar nicht hauptsächlich wegen der Deprecations. Attribute sind echte Sprachkonstrukte, die IDE löst sie also auf, Refactoring-Werkzeuge benennen sie um, und ein Tippfehler ist ein Fehler statt eines still ignorierten Kommentars.

Die komplette Umstellung, inklusive des Herausfindens der eigenen Mappings, hat einen Nachmittag gekostet, bei einer Codebase, in der es von Hand eine Woche gewesen wäre. Das ist ein guter Tausch — und solange die Konfiguration noch frisch im Kopf ist, ist es der richtige Moment, das übrige Qualitätswerkzeug drumherum aufzusetzen. Die Liste, die ich am ersten Tag einrichten würde, steht hier.

Sie brauchen einen Senior-Entwickler, der liefert?

Über 20 Jahre. Produktionssysteme. Echte Deadlines. Ob Architekt, Entwickler oder DevOps-Engineer — ich habe alle drei Rollen übernommen, oft im selben Projekt. Sparen wir uns den Agentur-Overhead und sprechen direkt.