Ga naar inhoud

Een aangepaste Rector-downgraderegel schrijven

De NextPDF-backportpijplijn downgradet de PHP 8.4-broncode van nextpdf/nextpdf, zodat die op PHP 8.1 kan draaien en, voor de kern, op PHP 7.4. Voor de meeste taalfuncties gebruikt de pijplijn de ingebouwde downgradesets van Rector, aangevuld met een kleine set aangepaste regels voor functies die Rector niet dekt.

Deze handleiding laat zien hoe u een nieuwe aangepaste regel toevoegt. De regel volgt dezelfde structuur als de drie regels die al in de repository aanwezig zijn: DowngradeAsymmetricVisibilityRector, DowngradeCloneWithRector en DowngradeTraitConstantsRector. Alle drie staan in rector/rules/, zijn geregistreerd in rector/config/ en hebben fixturegebaseerde tests in tests/Rector/.

Gebruik deze handleiding wanneer een NextPDF-functie PHP-syntaxis gebruikt die het buildtarget niet ondersteunt en Rector er geen ingebouwde downgrade voor heeft. Controleer voordat u begint of Rector echt geen regel heeft. De ingebouwde withDowngradeSets()-keten verwerkt al readonly-klassen, getypeerde klasseconstanten, de pipe-operator en veel andere functies.

U schrijft en draait aangepaste regels binnen de nextpdf-backport-repository. De bijbehorende composer.json declareert de ontwikkeltools.

  1. Kloon de backport-repository en installeer de afhankelijkheden ervan.
  2. Bevestig dat Rector en PHPUnit worden gevonden.
Terminal window
git clone https://github.com/nextpdf-labs/backport.git
cd backport
composer install
Terminal window
vendor/bin/rector --version
vendor/bin/phpunit --version

Voor het draaien van de tooling is PHP 8.4 vereist, omdat de regels 8.4-syntaxisbomen manipuleren, ook al richt de buildoutput zich op PHP 8.1 of 7.4. De composer.jsonrequire zet php vast op >=8.4 <9.0. Aangepaste regels worden automatisch geladen in de namespace NextPDF\Backport\, die verwijst naar rector/rules/; tests worden automatisch geladen in NextPDF\Backport\Tests\, die verwijst naar tests/.

Elke aangepaste regel breidt Rector\Rector\AbstractRector uit en implementeert drie methoden. Het contract is voor alle drie de bestaande regels gelijk.

LidTypeDoel
getRuleDefinition()RuleDefinitionEen leesbare beschrijving en before/after CodeSample-paren voor de generator van de regeldocumentatie.
getNodeTypes()array<class-string<Node>>De Abstract Syntax Tree (AST)-knoopklassen die de regel bezoekt. Rector roept refactor() alleen voor die klassen aan.
refactor(Node $node)?Node of `Stmt[]null`

De declaratie van het knooptype stuurt de regel aan. DowngradeAsymmetricVisibilityRector richt zich op [Property::class, Param::class] omdat asymmetrische zichtbaarheid (public private(set)) zowel op een klasse-eigenschap als op een gepromoveerde constructorparameter kan voorkomen. DowngradeTraitConstantsRector richt zich op [Trait_::class] omdat deze regel het hele trait-blok in één keer herschrijft. DowngradeCloneWithRector richt zich op [FileNode::class, Return_::class, Expression::class] omdat clone-with (clone($obj, [...])) zowel in return-posities als in toewijzingen kan voorkomen; deze regel gebruikt het bezoek aan FileNode om een teller per bestand te resetten.

Een regel die vanuit refactor() null retourneert, geeft „geen wijziging” aan. Een regel die een knoop retourneert, geeft een vervanging aan. Een regel die Stmt[] retourneert, een lijst met statements, breidt één statement uit tot meerdere. Zo zet DowngradeCloneWithRector één enkele return clone($this, [...]); om in een clone-toewijzing, één eigenschapstoewijzing per overschrijving en een afsluitende return.

De twee pijplijnconfiguraties roepen ->withDowngradeSets(php81: true) en ->withDowngradeSets(php74: true) aan. Die sets schakelen alle ingebouwde downgraderegels voor het target in. Aangepaste regels bestaan alleen voor de hiaten: asymmetrische zichtbaarheid in PHP 8.4, clone-with in PHP 8.5 en traitconstanten in PHP 8.2. Rector downgradet geen van deze functies uit zichzelf. Schrijf pas een aangepaste regel nadat u hetzelfde hiaat hebt bevestigd.

Deze procedure voegt een nieuwe regel toe. Het doorlopende voorbeeld weerspiegelt de bestaande regels; vervang dit door uw eigen functie.

  1. Maak de regelklasse aan in rector/rules/. Geef deze de naam Downgrade<Feature>Rector en plaats deze in de namespace NextPDF\Backport zodat de bestaande autoload-mapping deze oppikt.
  2. Breid Rector\Rector\AbstractRector uit en markeer de klasse als final.
  3. Implementeer getNodeTypes() zodat deze de smalst mogelijke set AST-knoopklassen retourneert die de regel nodig heeft. Met een smallere set bezoekt Rector minder knopen.
  4. Implementeer refactor(). Controleer met een assertie dat de knoop een van de gedeclareerde typen is, bewaak de exacte syntaxis waarop u zich richt, transformeer die syntaxis en retourneer de nieuwe knoop, Stmt[] of null voor geen wijziging.
  5. Implementeer getRuleDefinition() met één before/after CodeSample voor elk afzonderlijk geval dat de regel afhandelt.
  6. Houd het bestand op PHPStan Level 10: typeer elke parameter, retourwaarde en eigenschap, en gebruik PHPDoc-generics om de vorm van arrays te beschrijven.

De regel voor asymmetrische zichtbaarheid is het kleinste volledige voorbeeld. Deze regel verwijdert de set-zichtbaarheidsvlaggen en zorgt ervoor dat de basis-leeszichtbaarheid behouden blijft:

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;
}
}

De controle in de eerste if is cruciaal. Wanneer de eigenschap geen set-zichtbaarheidsvlag heeft, retourneert de regel null en laat deze de knoop ongewijzigd. Een regel die onvoorwaardelijk transformeert, zou code herschrijven die ongemoeid had moeten blijven.

Elke regel heeft een fixturegebaseerde test die de regel uitvoert op .php.inc-bestanden en controleert of de uitvoer overeenkomt. De harness komt uit Rector\Testing\PHPUnit\AbstractRectorTestCase van Rector.

Een testcase is klein en consistent voor de drie bestaande regels:

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';
}
}

De test verwijst naar een configuratiebestand per regel in tests/Rector/config/ dat alleen de regel onder test registreert, zodat elke fixture één regel geïsoleerd uitvoert:

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,
]);

Een fixture is een .php.inc-bestand met de invoer, een ------scheidingsteken en de verwachte uitvoer. Wanneer de regel geen wijziging aanbrengt, laat u het scheidingsteken en het tweede blok weg. Een transformerende fixture voor de traitconstantenregel ziet er als volgt uit:

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;
}
}
?>

Om de tests voor een nieuwe regel te schrijven:

  1. Maak tests/Rector/Fixtures/Downgrade<Feature>/ aan en voeg één .php.inc per geval toe.
  2. Dek zowel transformerende gevallen met een ------scheidingsteken als overgeslagen gevallen zonder scheidingsteken af, zoals een eigenschap zonder set-zichtbaarheid die ongewijzigd moet blijven.
  3. Voeg een tests/Rector/config/downgrade_<feature>.php toe die alleen uw regel registreert.
  4. Voeg een tests/Rector/Downgrade<Feature>RectorTest.php toe die de fixturemap oplevert en naar de configuratie verwijst.
  5. Voer de testsuite uit.
Terminal window
composer test

De repository bevat ook RectorRulesBehaviorTest en RectorRulesMetadataTest, die het gedrag over regels heen controleren en bevestigen dat de getRuleDefinition() van elke regel correct is opgebouwd. Voer de volledige composer test uit, zodat die controles uw nieuwe regel meenemen.

Een regel is pas actief in de build wanneer u deze registreert in de pijplijnconfiguraties. Er zijn twee buildtargets, elk met een eigen config in rector/config/.

  1. Open rector/config/rector-php81.php en voeg uw regelklasse toe aan de lijst ->withRules([...]).
  2. Als de functie ook moet worden gedowngraded voor de PHP 7.4-kernbuild, voeg dezelfde klasse dan toe aan rector/config/rector-php74.php.
  3. Voeg een commentaar toe met de PHP-versie waarin de functie is geïntroduceerd, in lijn met de bestaande vermeldingen.
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,
]);

De buildorkestrator (scripts/build.php) voegt de bronrepository’s samen, draait Rector met deze configs, past de gegenereerde composer.json aan en voert een php -l-syntaxiscontrole uit op de uitvoer. Controleer uw regel met PHPStan en de volledige build voordat u erop vertrouwt.

Terminal window
composer analyse
composer build:dry
  • De registratievolgorde maakt niet uit, maar de regelvolgorde conceptueel wel. Het multi-passmechanisme van Rector doorloopt de boom opnieuw totdat geen enkele regel nog een wijziging aanbrengt, dus u hoeft regels niet handmatig in de config te ordenen. Documenteer toch elke volgordeafhankelijkheid in het docblock van de klasse, zoals DowngradeCloneWithRector doet: de expansie ervan produceert $clone->prop = $val, wat op een readonly-eigenschap zou mislukken, dus DowngradeReadonlyPropertyRector moet voor hetzelfde target draaien.
  • Geef lege attributen door wanneer u een vervangende knoop bouwt vanuit een ander knooptype. DowngradeTraitConstantsRector bouwt een Property uit een ClassConst en geeft [] door als attributen in plaats van de attributen van de bronknoop. Als u de oorspronkelijke attributen overneemt, laat u een origNode-verwijzing naar het verkeerde knooptype achter en activeert u een assertie in de opmaakbehoudende printer.
  • Reset de status per bestand bij het bezoek aan FileNode. DowngradeCloneWithRector declareert FileNode::class in getNodeTypes() alleen om de teller voor tijdelijke variabelen aan het begin van elk bestand te resetten, zodat gegenereerde variabelenamen niet tussen bestanden botsen.
  • Bewaak nauwkeurig en retourneer dan null. Een clone-with-transformatie moet bevestigen dat de aanroepnaam clone is en dat het tweede argument een array-literal is voordat deze handelt; een gewone clone $obj bereikt de regel nooit als functieaanroep, en een aanroep met twee argumenten waarvan het tweede argument geen array is, blijft ongemoeid.
  • Verwijder modifiers die het target niet kan uitdrukken. Wanneer de traitconstantenregel een constante omzet in een statische eigenschap, behoudt deze de zichtbaarheid en voegt static toe, maar mag deze geen final-modifier dragen omdat PHP 8.1-eigenschappen niet final kunnen zijn.
  • Houd de regel op PHPStan Level 10. De repository draait composer analyse op niveau 10 over rector/rules en scripts. Typeer elke signature en annoteer arrayvormen; een regel die de analyser niet zou overleven, is een defect, geen concept.