Собственное правило понижения для Rector
Конвейер бэкпорта NextPDF понижает исходный код nextpdf/nextpdf с PHP 8.4 так, чтобы он работал на PHP 8.1, а для ядра — на PHP 7.4. Для большинства возможностей языка он использует встроенные наборы понижения Rector, а также небольшой набор собственных правил для возможностей, которые Rector не поддерживает.
В этом руководстве показано, как добавить новое собственное правило. Оно повторяет структуру трёх правил, которые уже есть в репозитории: DowngradeAsymmetricVisibilityRector, DowngradeCloneWithRector и DowngradeTraitConstantsRector. Все три находятся в rector/rules/, регистрируются в rector/config/ и имеют тесты на основе фикстур в tests/Rector/.
Используйте это руководство, когда возможность NextPDF использует синтаксис PHP, который не поддерживается целевой сборкой, а в Rector нет встроенного понижения для этой возможности. Прежде чем начать, убедитесь, что в Rector действительно нет нужного правила. Встроенная цепочка withDowngradeSets() уже обрабатывает readonly-классы, типизированные константы классов, оператор pipe и многие другие возможности.
Установка
Заголовок раздела «Установка»Собственные правила пишутся и запускаются внутри репозитория nextpdf-backport. Инструменты разработки объявлены в его composer.json.
- Клонируйте репозиторий бэкпорта и установите зависимости.
- Убедитесь, что Rector и PHPUnit разрешаются.
git clone https://github.com/nextpdf-labs/backport.gitcd backportcomposer installvendor/bin/rector --versionvendor/bin/phpunit --versionДля работы репозиторию требуется PHP 8.4, потому что правила работают с синтаксическими деревьями 8.4, хотя результат сборки рассчитан на PHP 8.1 или 7.4. composer.jsonrequire закрепляет php на >=8.4 <9.0. Собственные правила автоматически загружаются в пространстве имён NextPDF\Backport\, сопоставленном с rector/rules/; тесты автоматически загружаются в NextPDF\Backport\Tests\, сопоставленном с tests/.
Анатомия правила
Заголовок раздела «Анатомия правила»Каждое собственное правило наследует Rector\Rector\AbstractRector и реализует три метода. Контракт один и тот же для всех трёх существующих правил.
| Элемент | Тип | Назначение |
|---|---|---|
getRuleDefinition() | RuleDefinition | Читабельное описание и пары before/after CodeSample для генератора документации правил. |
getNodeTypes() | array<class-string<Node>> | Классы узлов абстрактного синтаксического дерева (AST), которые посещает правило. Rector вызывает refactor() только для этих классов. |
refactor(Node $node) | ?Node или `Stmt[] | null` |
Объявление типов узлов управляет правилом. DowngradeAsymmetricVisibilityRector нацелено на [Property::class, Param::class], потому что асимметричная видимость (public private(set)) может встречаться и в свойстве класса, и в продвигаемом параметре конструктора. DowngradeTraitConstantsRector нацелено на [Trait_::class], потому что переписывает всё тело трейта за один проход. DowngradeCloneWithRector нацелено на [FileNode::class, Return_::class, Expression::class], потому что clone-with (clone($obj, [...])) может встречаться как в позиции return, так и в присваивании; посещение FileNode используется для сброса счётчика в пределах файла.
Правило, возвращающее null из refactor(), сигнализирует “без изменений”. Правило, возвращающее узел, сигнализирует о замене. Правило, возвращающее Stmt[] — список операторов, — разворачивает один оператор в несколько. Именно так DowngradeCloneWithRector превращает один return clone($this, [...]); в присваивание клона, по одному присваиванию свойства на каждое переопределение и итоговый return.
Что уже делают встроенные наборы
Заголовок раздела «Что уже делают встроенные наборы»Две конфигурации конвейера вызывают ->withDowngradeSets(php81: true) и ->withDowngradeSets(php74: true). Эти наборы выстраивают в цепочку все встроенные правила понижения для целевой версии. Собственные правила нужны только для пробелов: асимметричная видимость PHP 8.4, clone-with PHP 8.5 и константы трейтов PHP 8.2. Ни одну из этих возможностей Rector не понижает самостоятельно. Пишите собственное правило только после того, как подтвердите такой же пробел.
Пошагово: как написать правило
Заголовок раздела «Пошагово: как написать правило»Эта процедура добавляет новое правило. Сквозной пример повторяет существующие правила; подставьте свою возможность.
- Создайте класс правила в
rector/rules/. Назовите егоDowngrade<Feature>Rectorи поместите в пространство имёнNextPDF\Backport, чтобы его подхватило существующее сопоставление автозагрузки. - Унаследуйте
Rector\Rector\AbstractRectorи отметьте класс какfinal. - Реализуйте
getNodeTypes()так, чтобы он возвращал самый узкий набор классов узлов AST, который нужен правилу. Чем уже набор, тем меньше узлов посещает Rector. - Реализуйте
refactor(). Убедитесь, что узел относится к одному из объявленных типов, проверьте точный целевой синтаксис, преобразуйте узел и верните новый узел,Stmt[]илиnull, если изменений нет. - Реализуйте
getRuleDefinition()с одной парой before/afterCodeSampleдля каждого отдельного случая, который обрабатывает правило. - Поддерживайте файл на уровне PHPStan Level 10: типизируйте каждый параметр, возвращаемое значение и свойство, а для описания форм массивов используйте дженерики PHPDoc.
Правило для asymmetric-visibility — самый компактный полный пример. Оно удаляет флаги видимости записи и следит за тем, чтобы базовая видимость чтения сохранялась:
<?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; }}Проверка в первом if критична. Когда у свойства нет флага видимости записи, правило возвращает null и оставляет узел без изменений. Правило, которое выполняет преобразование безусловно, переписало бы код, который должно было оставить нетронутым.
Фикстуры и тестирование
Заголовок раздела «Фикстуры и тестирование»У каждого правила есть тест на основе фикстур: он прогоняет правило по файлам .php.inc и проверяет совпадение результата. Тестовая обвязка берётся из Rector\Testing\PHPUnit\AbstractRectorTestCase в Rector.
Тестовый класс невелик и одинаков для всех трёх существующих правил:
<?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'; }}Тест указывает на отдельный для каждого правила файл конфигурации в tests/Rector/config/. Он регистрирует только проверяемое правило, поэтому каждая фикстура задействует одно правило изолированно:
<?php
declare(strict_types=1);
use NextPDF\Backport\DowngradeTraitConstantsRector;use Rector\Config\RectorConfig;
return RectorConfig::configure() ->withRules([ DowngradeTraitConstantsRector::class, ]);Фикстура — это файл .php.inc с входными данными, разделителем ----- и ожидаемым результатом. Если правило не вносит изменений, опустите разделитель и второй блок. Преобразующая фикстура для правила trait-constants выглядит так:
<?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; }}?>Чтобы написать тесты для нового правила:
- Создайте
tests/Rector/Fixtures/Downgrade<Feature>/и добавьте по одному.php.incна каждый случай. - Охватите как преобразующие случаи с разделителем
-----, так и пропускаемые случаи без разделителя — например, свойство без видимости записи, которое должно остаться без изменений. - Добавьте
tests/Rector/config/downgrade_<feature>.php, который регистрирует только ваше правило. - Добавьте
tests/Rector/Downgrade<Feature>RectorTest.php, который отдаёт каталог фикстур и указывает на конфигурацию. - Запустите набор тестов.
composer testРепозиторий также включает RectorRulesBehaviorTest и RectorRulesMetadataTest. Они проверяют взаимодействие правил и подтверждают, что getRuleDefinition() каждого правила сформирован правильно. Запустите полный composer test, чтобы эти проверки увидели ваше новое правило.
Подключение к сборке
Заголовок раздела «Подключение к сборке»Правило не действует в сборке, пока вы не зарегистрируете его в конфигурациях конвейера. Есть две целевые сборки, и у каждой своя конфигурация в rector/config/.
- Откройте
rector/config/rector-php81.phpи добавьте класс своего правила в список->withRules([...]). - Если возможность нужно понижать и для сборки ядра под PHP 7.4, добавьте тот же класс в
rector/config/rector-php74.php. - Добавьте комментарий с указанием версии 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, ]);Оркестратор сборки (scripts/build.php) объединяет исходные репозитории, запускает Rector с этими конфигурациями, корректирует сгенерированный composer.json и проверяет синтаксис результата через php -l. Проверьте своё правило с помощью PHPStan и полной сборки, прежде чем полагаться на него.
composer analysecomposer build:dryГраничные случаи и подводные камни
Заголовок раздела «Граничные случаи и подводные камни»- Порядок регистрации не имеет значения, но концептуально порядок правил важен. Многопроходный механизм Rector повторяет обход, пока ни одно правило не вносит очередное изменение, поэтому вручную упорядочивать правила в конфигурации не нужно. Тем не менее задокументируйте любую зависимость от порядка в докблоке класса, как это делает
DowngradeCloneWithRector: его развёртывание порождает$clone->prop = $val, что завершилось бы ошибкой на readonly-свойстве, поэтомуDowngradeReadonlyPropertyRectorдолжно выполняться для той же целевой версии. - Передавайте пустые атрибуты, когда строите узел-замену из узла другого вида.
DowngradeTraitConstantsRectorстроитPropertyизClassConstи передаёт[]в качестве атрибутов вместо атрибутов исходного узла. Если перенести исходные атрибуты, вы оставите указательorigNodeна узел неправильного вида и нарушите assert в принтере, сохраняющем форматирование. - Сбрасывайте состояние уровня файла при посещении
FileNode.DowngradeCloneWithRectorобъявляетFileNode::classвgetNodeTypes()только для сброса счётчика временных переменных в начале каждого файла, чтобы сгенерированные имена переменных не конфликтовали между файлами. - Проверяйте точно, затем возвращайте
null. Перед выполнением преобразование clone-with должно подтвердить, что имя вызова —cloneи что второй аргумент — литерал массива; обычныйclone $objвообще не доходит до правила как вызов функции, а вызов с двумя аргументами, второй из которых не массив, остаётся нетронутым. - Убирайте модификаторы, которые целевая версия не может выразить. Когда правило trait-constants превращает константу в статическое свойство, оно сохраняет видимость и добавляет
static, но не должно переносить модификаторfinal, потому что свойства в PHP 8.1 не могут быть final. - Поддерживайте правило на уровне PHPStan Level 10. Репозиторий запускает
composer analyseна уровне 10 поrector/rulesиscripts. Типизируйте каждую сигнатуру и аннотируйте формы массивов; правило, которое не прошло бы анализатор, — дефект, а не черновик.
См. также
Заголовок раздела «См. также»- Руководство разработчика Backport Builder — архитектура конвейера, модель ветвей и артефакты выпуска, связанные с этими правилами.
- Справочник по API Backport — публичный интерфейс инструментария сборки бэкпорта.
- Конфигурация Backport — целевые сборки и выбор наборов понижения.
- Устранение неполадок Backport — диагностика сбоев в сгенерированном дереве понижения.
- Документация Rector — первоисточник по
AbstractRector,RuleDefinitionи классам узлов AST, используемым вgetNodeTypes().