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.

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.