Zum Inhalt springen

Eine eigene Rector-Downgrade-Regel erstellen

Die NextPDF-Backport-Pipeline führt für den PHP-8.4-Quellcode von nextpdf/nextpdf ein Downgrade durch, sodass er unter PHP 8.1 (und beim Core auch unter PHP 7.4) läuft. Für die meisten Sprachfunktionen verwendet sie Rectors eingebaute Downgrade-Sets sowie einige eigene Regeln für Funktionen, die Rector nicht von Haus aus abdeckt.

Diese Anleitung zeigt Ihnen, wie Sie eine neue eigene Regel hinzufügen. Sie folgt dem Aufbau der drei bereits im Repository vorhandenen Regeln: DowngradeAsymmetricVisibilityRector, DowngradeCloneWithRector und DowngradeTraitConstantsRector. Alle drei liegen in rector/rules/, sind in rector/config/ registriert und werden durch Fixture-basierte Tests in tests/Rector/ abgedeckt.

Verwenden Sie diese Anleitung, wenn ein NextPDF-Feature PHP-Syntax einsetzt, die vom Build-Ziel nicht unterstützt wird, und Rector kein eingebautes Downgrade dafür bietet. Stellen Sie vorab sicher, dass Rector dafür wirklich keine Regel enthält. Die eingebaute withDowngradeSets()-Kette behandelt bereits readonly-Klassen, typisierte Klassenkonstanten, den Pipe-Operator und viele weitere Funktionen.

Eigene Regeln schreiben und führen Sie innerhalb des nextpdf-backport-Repositorys aus. Die Entwicklungswerkzeuge sind in dessen composer.json deklariert.

  1. Klonen Sie das Backport-Repository und installieren Sie seine Abhängigkeiten.
  2. Stellen Sie sicher, dass Rector und PHPUnit aufgelöst werden.
Terminal-Fenster
git clone https://github.com/nextpdf-labs/backport.git
cd backport
composer install
Terminal-Fenster
vendor/bin/rector --version
vendor/bin/phpunit --version

Das Repository benötigt für die Ausführung PHP 8.4 (die Regeln manipulieren 8.4-Syntaxbäume), auch wenn die Build-Ausgabe auf PHP 8.1 oder 7.4 zielt. In composer.jsonrequire ist php auf >=8.4 <9.0 festgelegt. Eigene Regeln werden über den Namespace NextPDF\Backport\ autogeladen, der auf rector/rules/ abgebildet ist; Tests werden über NextPDF\Backport\Tests\ autogeladen und auf tests/ abgebildet.

Jede eigene Regel erweitert Rector\Rector\AbstractRector und implementiert drei Methoden. Der Vertrag ist über alle drei bestehenden Regeln hinweg derselbe.

ElementTypZweck
getRuleDefinition()RuleDefinitionMenschenlesbare Beschreibung sowie before/after-CodeSample-Paare für den Regeldokumentations-Generator.
getNodeTypes()array<class-string<Node>>Die AST-Knotenklassen (Abstract Syntax Tree), die die Regel besuchen soll. Rector ruft refactor() nur für diese auf.
refactor(Node $node)?Node oder `Stmt[]null`

Die Knotentyp-Deklaration steuert alles. DowngradeAsymmetricVisibilityRector zielt auf [Property::class, Param::class], weil asymmetrische Sichtbarkeit (public private(set)) sowohl an einer Klasseneigenschaft als auch an einem promoteten Konstruktorparameter auftreten kann. DowngradeTraitConstantsRector zielt auf [Trait_::class], weil es den gesamten Trait-Körper in einem Durchgang umschreibt. DowngradeCloneWithRector zielt auf [FileNode::class, Return_::class, Expression::class], weil clone-with (clone($obj, [...])) sowohl in return- als auch in Zuweisungspositionen vorkommt; den FileNode-Besuch nutzt es, um einen Zähler pro Datei zurückzusetzen.

Eine Regel, die null aus refactor() zurückgibt, signalisiert „keine Änderung“. Eine Regel, die einen Knoten zurückgibt, signalisiert eine Ersetzung. Eine Regel, die Stmt[] (eine Liste von Statements) zurückgibt, weitet ein Statement auf mehrere aus. So macht DowngradeCloneWithRector aus einem einzelnen return clone($this, [...]); eine Klon-Zuweisung, je eine Eigenschaftszuweisung pro Override und ein abschließendes return.

Die beiden Pipeline-Konfigurationen rufen ->withDowngradeSets(php81: true) und ->withDowngradeSets(php74: true) auf. Diese Sets verketten alle eingebauten Downgrade-Regeln für das jeweilige Ziel. Eigene Regeln gibt es nur für die Lücken: asymmetrische Sichtbarkeit aus PHP 8.4, clone-with aus PHP 8.5 und Trait-Konstanten aus PHP 8.2. Rector downgradet keine davon von sich aus. Schreiben Sie eine eigene Regel erst, nachdem Sie dieselbe Lücke bestätigt haben.

Dieses Verfahren fügt eine neue Regel hinzu. Das durchgängige Beispiel spiegelt den Aufbau der bestehenden Regeln wider; setzen Sie Ihr eigenes Feature ein.

  1. Erstellen Sie die Regelklasse in rector/rules/. Benennen Sie sie Downgrade<Feature>Rector und legen Sie sie im Namespace NextPDF\Backport ab, damit die bestehende Autoload-Abbildung sie erfasst.
  2. Erweitern Sie Rector\Rector\AbstractRector und markieren Sie die Klasse als final.
  3. Implementieren Sie getNodeTypes() so, dass es die engste Menge an AST-Knotenklassen zurückgibt, die die Regel benötigt. Eine engere Menge bedeutet, dass Rector weniger Knoten besucht.
  4. Implementieren Sie refactor(). Stellen Sie sicher, dass der Knoten einer der deklarierten Typen ist, sichern Sie genau die Syntax ab, auf die Sie zielen, transformieren Sie ihn und geben Sie den neuen Knoten zurück (oder Stmt[] oder null bei keiner Änderung).
  5. Implementieren Sie getRuleDefinition() mit einem before/after CodeSample pro eigenständigem Fall, den die Regel behandelt.
  6. Halten Sie die Datei auf PHPStan-Level 10: Jeder Parameter, jeder Rückgabewert und jede Eigenschaft ist typisiert, und PHPDoc-Generics beschreiben die Array-Formen.

Die Regel für asymmetrische Sichtbarkeit ist das kleinste vollständige Beispiel. Sie entfernt die Set-Sichtbarkeitsflags und stellt sicher, dass eine Basis-Lesesichtbarkeit erhalten bleibt:

rector/rules/DowngradeAsymmetricVisibilityRector.php
<?php
declare(strict_types=1);
namespace NextPDF\Backport;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\Property;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
final class DowngradeAsymmetricVisibilityRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Remove asymmetric visibility modifiers (public private(set) -> public)',
[
new CodeSample(
'public private(set) float $x = 0.0;',
'public float $x = 0.0;',
),
],
);
}
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [Property::class, Param::class];
}
public function refactor(Node $node): ?Node
{
\assert($node instanceof Property || $node instanceof Param);
if (($node->flags & Modifiers::VISIBILITY_SET_MASK) === 0) {
return null;
}
$node->flags &= ~Modifiers::VISIBILITY_SET_MASK;
if (($node->flags & Modifiers::VISIBILITY_MASK) === 0) {
$node->flags |= Modifiers::PUBLIC;
}
return $node;
}
}

Die Bedingung im ersten if ist der entscheidende Teil. Wenn die Eigenschaft kein Set-Sichtbarkeitsflag hat, gibt die Regel null zurück und lässt den Knoten unverändert. Eine Regel, die bedingungslos transformiert, würde Code umschreiben, den sie nicht anfassen sollte.

Jede Regel hat einen Fixture-basierten Test, der die Regel gegen .php.inc-Dateien ausführt und prüft, ob die Ausgabe übereinstimmt. Der Harness stammt aus Rectors Rector\Testing\PHPUnit\AbstractRectorTestCase.

Ein Testfall ist kompakt und über die drei bestehenden Regeln hinweg einheitlich aufgebaut:

tests/Rector/DowngradeTraitConstantsRectorTest.php
<?php
declare(strict_types=1);
namespace NextPDF\Backport\Tests\Rector;
use Iterator;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class DowngradeTraitConstantsRectorTest extends AbstractRectorTestCase
{
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}
public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixtures/DowngradeTraitConstants');
}
public function provideConfigFilePath(): string
{
return __DIR__ . '/config/downgrade_trait_constants.php';
}
}

Der Test verweist auf eine Konfigurationsdatei pro Regel in tests/Rector/config/, die nur die zu testende Regel registriert, sodass ein Fixture eine einzelne Regel isoliert durchläuft:

tests/Rector/config/downgrade_trait_constants.php
<?php
declare(strict_types=1);
use NextPDF\Backport\DowngradeTraitConstantsRector;
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withRules([
DowngradeTraitConstantsRector::class,
]);

Ein Fixture ist eine .php.inc-Datei mit der Eingabe, einem ------Trenner und der erwarteten Ausgabe. Wenn die Regel keine Änderung vornimmt, lassen Sie den Trenner und den zweiten Block weg. Ein transformierendes Fixture für die Trait-Konstanten-Regel sieht so aus:

tests/Rector/Fixtures/DowngradeTraitConstants/private_constant.php.inc
<?php
trait HasLimit
{
private const MAX_SIZE = 1024;
public function getLimit(): int
{
return self::MAX_SIZE;
}
}
?>
-----
<?php
trait HasLimit
{
private static $MAX_SIZE = 1024;
public function getLimit(): int
{
return self::$MAX_SIZE;
}
}
?>

So legen Sie die Tests einer neuen Regel an:

  1. Erstellen Sie tests/Rector/Fixtures/Downgrade<Feature>/ und fügen Sie pro Fall ein .php.inc hinzu.
  2. Decken Sie sowohl transformierende Fälle (mit einem ------Trenner) als auch Skip-Fälle (ohne Trenner) ab – zum Beispiel muss eine Eigenschaft ohne Set-Sichtbarkeit unverändert durchlaufen.
  3. Fügen Sie eine tests/Rector/config/downgrade_<feature>.php hinzu, die nur Ihre Regel registriert.
  4. Fügen Sie eine tests/Rector/Downgrade<Feature>RectorTest.php hinzu, die das Fixture-Verzeichnis liefert und auf die Konfiguration verweist.
  5. Führen Sie die Suite aus.
Terminal-Fenster
composer test

Das Repository enthält außerdem RectorRulesBehaviorTest und RectorRulesMetadataTest; sie prüfen regelübergreifendes Verhalten und stellen sicher, dass die getRuleDefinition() jeder Regel wohlgeformt ist. Führen Sie das vollständige composer test aus, damit diese Gates Ihre neue Regel erfassen.

Eine Regel ist im Build nicht aktiv, solange sie nicht in den Pipeline-Konfigurationen registriert ist. Es gibt zwei Build-Ziele, jedes mit seiner eigenen Konfiguration in rector/config/.

  1. Öffnen Sie rector/config/rector-php81.php und fügen Sie Ihre Regelklasse zur ->withRules([...])-Liste hinzu.
  2. Wenn das Feature auch für den PHP-7.4-Core-Build downgegradet werden muss, fügen Sie dieselbe Klasse zu rector/config/rector-php74.php hinzu.
  3. Fügen Sie einen Kommentar hinzu, der die PHP-Version nennt, in der das Feature eingeführt wurde – passend zu den bestehenden Einträgen.
rector/config/rector-php81.php
<?php
declare(strict_types=1);
use NextPDF\Backport\DowngradeAsymmetricVisibilityRector;
use NextPDF\Backport\DowngradeCloneWithRector;
use NextPDF\Backport\DowngradeTraitConstantsRector;
use Rector\Config\RectorConfig;
use Rector\DowngradePhp81\Rector\Property\DowngradeReadonlyPropertyRector;
return RectorConfig::configure()
->withDowngradeSets(php81: true)
->withRules([
// PHP 8.4 — asymmetric visibility (not covered by built-in sets)
DowngradeAsymmetricVisibilityRector::class,
// PHP 8.4 — clone-with syntax (not covered by built-in sets)
DowngradeCloneWithRector::class,
// PHP 8.2 — trait constants (not covered by built-in sets)
DowngradeTraitConstantsRector::class,
// PHP 8.1 — readonly property removal (for clone-with expansion safety)
DowngradeReadonlyPropertyRector::class,
]);

Der Build-Orchestrator (scripts/build.php) führt die Quell-Repositorys zusammen, lässt Rector mit diesen Konfigurationen laufen, passt die generierte composer.json an und führt eine php -l-Syntaxprüfung über die Ausgabe aus. Prüfen Sie Ihre Regel gegen PHPStan und den vollständigen Build, bevor Sie sich darauf verlassen.

Terminal-Fenster
composer analyse
composer build:dry
  • Die Registrierungsreihenfolge spielt keine Rolle, die Regelreihenfolge konzeptionell aber schon. Rectors Multi-Pass-Mechanismus durchläuft den Baum erneut, bis keine Regel mehr eine weitere Änderung vornimmt, sodass Sie die Regeln in der Konfiguration nicht von Hand ordnen müssen. Dokumentieren Sie trotzdem jede Reihenfolgenabhängigkeit im Klassen-Docblock, wie es DowngradeCloneWithRector tut: Seine Expansion erzeugt $clone->prop = $val, was bei einer readonly-Eigenschaft fehlschlagen würde, sodass DowngradeReadonlyPropertyRector für dasselbe Ziel laufen muss.
  • Übergeben Sie leere Attribute, wenn Sie einen Ersatzknoten aus einer anderen Knotenart bauen. DowngradeTraitConstantsRector baut eine Property aus einer ClassConst und übergibt [] als Attribute statt der Attribute des Quellknotens. Die ursprünglichen Attribute zu übernehmen würde einen origNode-Zeiger auf die falsche Knotenart hinterlassen und eine Assertion im formaterhaltenden Printer auslösen.
  • Setzen Sie den Dateizustand beim FileNode-Besuch zurück. DowngradeCloneWithRector deklariert FileNode::class in getNodeTypes() allein dazu, seinen Zähler für temporäre Variablen zu Beginn jeder Datei zurückzusetzen, damit generierte Variablennamen nicht über Dateien hinweg kollidieren.
  • Sichern Sie präzise ab und geben Sie dann null zurück. Eine clone-with-Transformation muss prüfen, dass der Aufrufname clone lautet und dass das zweite Argument ein Array-Literal ist, bevor sie eine Änderung vornimmt; ein einfaches clone $obj erreicht die Regel nie als Funktionsaufruf, und ein zweiargumentiger Aufruf, dessen zweites Argument kein Array ist, bleibt unangetastet.
  • Entfernen Sie Modifikatoren, die das Ziel nicht ausdrücken kann. Wenn die Trait-Konstanten-Regel eine Konstante in eine statische Eigenschaft umwandelt, erhält sie die Sichtbarkeit und ergänzt static, darf aber keinen final-Modifikator tragen, weil PHP-8.1-Eigenschaften nicht final sein können.
  • Halten Sie die Regel auf PHPStan-Level 10. Das Repository führt composer analyse auf Level 10 über rector/rules und scripts aus. Typisieren Sie jede Signatur und annotieren Sie Array-Formen; eine Regel, die den Analyser nicht überstehen würde, ist ein Defekt, kein Entwurf.
  • Backport-Builder-Entwicklerhandbuch – die Pipeline-Architektur, das Branch-Modell und die Release-Artefakte rund um diese Regeln.
  • Backport-API-Referenz – die öffentliche Schnittstelle der Backport-Build-Werkzeuge.
  • Backport-Konfiguration – Build-Ziele und die Auswahl der Downgrade-Sets.
  • Backport-Fehlerbehebung – die Diagnose von Fehlern in einem generierten Downgrade-Baum.
  • Rector-Dokumentation – die Upstream-Referenz für AbstractRector, RuleDefinition und die in getNodeTypes() verwendeten AST-Knotenklassen.