การเขียนกฎ Rector downgrade แบบกำหนดเอง
ภาพรวมโดยย่อ
หัวข้อที่มีชื่อว่า “ภาพรวมโดยย่อ”NextPDF backport pipeline จะ downgrade ซอร์ส PHP 8.4 ของ nextpdf/nextpdf เพื่อให้ทำงานบน PHP 8.1 ได้ และให้ core ทำงานบน PHP 7.4 ได้ด้วย pipeline นี้ใช้ downgrade set ในตัวของ Rector สำหรับฟีเจอร์ภาษาส่วนใหญ่ ควบคู่กับชุดกฎกำหนดเองขนาดเล็กสำหรับฟีเจอร์ที่ Rector ยังไม่ครอบคลุม
คู่มือนี้อธิบายวิธีเพิ่มกฎกำหนดเองใหม่ โดยใช้โครงสร้างเดียวกับกฎสามตัวที่มีอยู่แล้วใน repository ได้แก่ DowngradeAsymmetricVisibilityRector, DowngradeCloneWithRector และ DowngradeTraitConstantsRector ทั้งสามตัวอยู่ใน rector/rules/ ลงทะเบียนไว้ใน rector/config/ และมีการทดสอบแบบ fixture อยู่ใน tests/Rector/
ใช้คู่มือนี้เมื่อฟีเจอร์ของ NextPDF ใช้ไวยากรณ์ PHP ที่ build target ไม่รองรับ และ Rector ไม่มี downgrade ในตัวสำหรับฟีเจอร์นั้น ก่อนเริ่ม ให้ยืนยันว่า Rector ยังไม่มีกฎดังกล่าวจริงๆ เพราะ withDowngradeSets() ในตัวรองรับ readonly class, typed class constant, pipe operator และฟีเจอร์อื่นๆ อีกมากอยู่แล้ว
การติดตั้ง
หัวข้อที่มีชื่อว่า “การติดตั้ง”เขียนและรันกฎกำหนดเองภายใน repository nextpdf-backport โดย composer.json ของ repository นี้ประกาศเครื่องมือสำหรับการพัฒนาไว้แล้ว
- โคลน backport repository และติดตั้ง dependency ของ repository นั้น
- ตรวจสอบว่า Rector และ PHPUnit ถูก resolve เรียบร้อยแล้ว
git clone https://github.com/nextpdf-labs/backport.gitcd backportcomposer installvendor/bin/rector --versionvendor/bin/phpunit --versionrepository ต้องใช้ PHP 8.4 ในการรัน เพราะกฎเหล่านี้จัดการกับ syntax tree ของ 8.4 แม้ว่าผลลัพธ์ของ build จะกำหนดเป้าหมายเป็น PHP 8.1 หรือ 7.4 ก็ตาม composer.jsonrequire ตรึง php ไว้ที่ >=8.4 <9.0 กฎกำหนดเอง autoload ภายใต้ namespace NextPDF\Backport\ ซึ่ง map ไปยัง rector/rules/ ส่วนการทดสอบ autoload ภายใต้ NextPDF\Backport\Tests\ ซึ่ง map ไปยัง tests/
โครงสร้างของกฎ
หัวข้อที่มีชื่อว่า “โครงสร้างของกฎ”กฎกำหนดเองทุกตัว extends Rector\Rector\AbstractRector และ implement เมท็อดสามรายการต่อไปนี้ กฎทั้งสามตัวที่มีอยู่ใช้ contract เดียวกัน
| สมาชิก | ชนิด | วัตถุประสงค์ |
|---|---|---|
getRuleDefinition() | RuleDefinition | คำอธิบายที่อ่านเข้าใจได้ พร้อมคู่ before/after CodeSample สำหรับตัวสร้างเอกสารของกฎ |
getNodeTypes() | array<class-string<Node>> | คลาส node ของ Abstract Syntax Tree (AST) ที่กฎเข้าไปตรวจ Rector จะเรียก refactor() เฉพาะกับคลาสเหล่านี้เท่านั้น |
refactor(Node $node) | ?Node หรือ `Stmt[] | null` |
การประกาศชนิด node คือสิ่งที่ขับเคลื่อนกฎ DowngradeAsymmetricVisibilityRector กำหนดเป้าหมายเป็น [Property::class, Param::class] เพราะ asymmetric visibility (public private(set)) อาจปรากฏได้ทั้งบน property ของคลาสและบนพารามิเตอร์คอนสตรักเตอร์แบบ promoted DowngradeTraitConstantsRector กำหนดเป้าหมายเป็น [Trait_::class] เพราะต้องเขียนทับเนื้อหาทั้งหมดของ trait ในรอบเดียว DowngradeCloneWithRector กำหนดเป้าหมายเป็น [FileNode::class, Return_::class, Expression::class] เพราะ clone-with (clone($obj, [...])) อาจปรากฏได้ทั้งในตำแหน่ง return และตำแหน่งของการกำหนดค่า โดยใช้การ visit FileNode เพื่อรีเซ็ตตัวนับแบบต่อไฟล์
กฎที่คืนค่า null จาก refactor() หมายถึง “ไม่มีการเปลี่ยนแปลง” กฎที่คืนค่า node หมายถึงมีการแทนที่ ส่วนกฎที่คืนค่า Stmt[] ซึ่งเป็นรายการของ statement จะขยายหนึ่ง statement ออกเป็นหลายรายการ นี่คือวิธีที่ DowngradeCloneWithRector เปลี่ยน return clone($this, [...]); เพียงรายการเดียวให้เป็นการกำหนดค่า clone ตามด้วยการกำหนดค่า property หนึ่งรายการต่อหนึ่ง override และ return สุดท้าย
สิ่งที่ set ในตัวจัดการให้อยู่แล้ว
หัวข้อที่มีชื่อว่า “สิ่งที่ set ในตัวจัดการให้อยู่แล้ว”config ของ pipeline ทั้งสองตัวเรียก ->withDowngradeSets(php81: true) และ ->withDowngradeSets(php74: true) set เหล่านั้นจะเรียกใช้กฎ downgrade ในตัวทุกตัวสำหรับ target ดังกล่าว กฎกำหนดเองมีไว้เฉพาะสำหรับช่องว่างเท่านั้น ได้แก่ asymmetric visibility ของ PHP 8.4, clone-with ของ PHP 8.5 และ trait constants ของ PHP 8.2 Rector ไม่ downgrade รายการเหล่านี้ด้วยตัวเอง ให้เขียนกฎกำหนดเองหลังจากยืนยันแล้วว่ามีช่องว่างลักษณะเดียวกันเท่านั้น
ทีละขั้นตอน: เขียนกฎ
หัวข้อที่มีชื่อว่า “ทีละขั้นตอน: เขียนกฎ”ขั้นตอนนี้ใช้สำหรับเพิ่มกฎใหม่ ตัวอย่างประกอบสะท้อนรูปแบบของกฎที่มีอยู่เดิม ให้แทนที่ด้วยฟีเจอร์ที่ต้องการรองรับ
- สร้างคลาสของกฎใน
rector/rules/ตั้งชื่อว่าDowngrade<Feature>Rectorและวางไว้ใน namespaceNextPDF\Backportเพื่อให้การ map autoload ที่มีอยู่เดิมรับไปใช้ได้ - extend
Rector\Rector\AbstractRectorและทำเครื่องหมายคลาสเป็นfinalด้วย - implement
getNodeTypes()ให้คืนชุดคลาส node ของ AST ที่แคบที่สุดเท่าที่กฎต้องการ ชุดที่แคบกว่าจะทำให้ Rector visit node น้อยลง - implement
refactor()โดย assert ว่า node เป็นหนึ่งในชนิดที่ประกาศไว้ ใส่ guard สำหรับไวยากรณ์ที่ต้องการกำหนดเป้าหมายโดยเฉพาะ แปลงไวยากรณ์นั้น แล้วคืนค่า node ใหม่Stmt[]หรือnullเมื่อไม่มีการเปลี่ยนแปลง - implement
getRuleDefinition()โดยมี before/afterCodeSampleหนึ่งชุดสำหรับแต่ละกรณีที่กฎจัดการแตกต่างกัน - รักษาไฟล์ให้อยู่ที่ PHPStan Level 10: กำหนดชนิดให้กับทุกพารามิเตอร์ ค่าคืน และ property พร้อมใช้ PHPDoc generics เพื่ออธิบายรูปแบบของอาร์เรย์
กฎ asymmetric-visibility เป็นตัวอย่างที่สมบูรณ์และเล็กที่สุด กฎนี้ลบ flag ของ set-visibility และตรวจสอบว่ายังคงมี read 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; }}การ guard ที่ if ตัวแรกมีความสำคัญอย่างยิ่ง เมื่อ property ไม่มี flag ของ set-visibility กฎจะคืนค่า null และปล่อยให้ node คงเดิม กฎที่แปลงโดยไม่มีเงื่อนไขจะเขียนทับโค้ดที่ควรปล่อยไว้ตามเดิม
Fixture และการทดสอบ
หัวข้อที่มีชื่อว่า “Fixture และการทดสอบ”กฎแต่ละตัวมีการทดสอบแบบ fixture ที่รันกฎกับไฟล์ .php.inc และ assert ว่าผลลัพธ์ตรงกัน harness มาจาก Rector\Testing\PHPUnit\AbstractRectorTestCase ของ Rector
test case มีขนาดเล็กและใช้รูปแบบสอดคล้องกันในกฎทั้งสามตัวที่มีอยู่:
<?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'; }}การทดสอบชี้ไปยังไฟล์ config แบบต่อกฎใน tests/Rector/config/ ซึ่งลงทะเบียนเฉพาะกฎที่กำลังทดสอบเท่านั้น ดังนั้นแต่ละ fixture จึงทดสอบกฎเพียงตัวเดียวแบบแยกอิสระ:
<?php
declare(strict_types=1);
use NextPDF\Backport\DowngradeTraitConstantsRector;use Rector\Config\RectorConfig;
return RectorConfig::configure() ->withRules([ DowngradeTraitConstantsRector::class, ]);fixture คือไฟล์ .php.inc ที่มีอินพุต ตัวคั่น ----- และผลลัพธ์ที่คาดหวัง เมื่อกฎไม่ได้ทำการเปลี่ยนแปลง ให้ละตัวคั่นและบล็อกที่สองไว้ fixture ที่มีการแปลงสำหรับกฎ 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หนึ่งไฟล์ต่อหนึ่งกรณี - ครอบคลุมทั้งกรณีที่มีการแปลงด้วยตัวคั่น
-----และกรณีที่ข้ามโดยไม่มีตัวคั่น เช่น property ที่ไม่มี set visibility ซึ่งต้องผ่านไปโดยไม่เปลี่ยนแปลง - เพิ่ม
tests/Rector/config/downgrade_<feature>.phpที่ลงทะเบียนเฉพาะกฎใหม่เท่านั้น - เพิ่ม
tests/Rector/Downgrade<Feature>RectorTest.phpที่ yield ไดเรกทอรี fixture และชี้ไปที่ config - รันชุดทดสอบ
composer testrepository ยังมี RectorRulesBehaviorTest และ RectorRulesMetadataTest ซึ่ง assert พฤติกรรมข้ามกฎ และยืนยันว่า getRuleDefinition() ของกฎแต่ละตัวมีรูปแบบที่ถูกต้อง ให้รัน composer test แบบเต็ม เพื่อให้ gate เหล่านั้นเห็นกฎใหม่
การเชื่อมเข้ากับ build
หัวข้อที่มีชื่อว่า “การเชื่อมเข้ากับ build”กฎจะยังไม่ทำงานใน build จนกว่าจะลงทะเบียนใน config ของ pipeline มี build target สองตัว และแต่ละตัวมี config ของตัวเองใน rector/config/ ดังนี้
- เปิด
rector/config/rector-php81.phpและเพิ่มคลาสของกฎใหม่ลงในรายการ->withRules([...]) - หากฟีเจอร์ต้องถูก downgrade สำหรับ core build ของ 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, ]);ตัว orchestrator ของ build (scripts/build.php) จะ merge ซอร์สจาก repository รัน Rector ด้วย config เหล่านี้ ปรับ composer.json ที่สร้างขึ้น และรันการตรวจไวยากรณ์ php -l ทั่วผลลัพธ์ ตรวจสอบกฎใหม่ด้วย PHPStan และการ build แบบเต็มก่อนนำไปใช้จริง
composer analysecomposer build:dryกรณีขอบและข้อควรระวัง
หัวข้อที่มีชื่อว่า “กรณีขอบและข้อควรระวัง”- ลำดับการลงทะเบียนไม่สำคัญ แต่ลำดับของกฎสำคัญในเชิงแนวคิด กลไกแบบหลายรอบ (multi-pass) ของ Rector จะวนซ้ำจนกว่าจะไม่มีกฎใดทำการเปลี่ยนแปลงอีก ดังนั้นจึงไม่ต้องเรียงลำดับกฎด้วยมือใน config อย่างไรก็ตาม ให้บันทึก dependency ด้านลำดับใดๆ ไว้ใน docblock ของคลาส เช่นที่
DowngradeCloneWithRectorทำไว้: การขยายของกฎนี้สร้าง$clone->prop = $valซึ่งจะล้มเหลวบน readonly property ดังนั้นDowngradeReadonlyPropertyRectorจึงต้องรันสำหรับ target เดียวกัน - ส่ง attribute ว่างเมื่อสร้าง node แทนที่จาก node ต่างชนิดกัน
DowngradeTraitConstantsRectorสร้างPropertyจากClassConstและส่ง[]เป็น attribute แทนการใช้ attribute ของ node ต้นทาง หากสืบทอด attribute เดิมมา ตัวชี้origNodeจะชี้ไปยัง node ผิดชนิด และไปสะดุด assertion ใน printer ที่รักษารูปแบบ (format-preserving printer) - รีเซ็ตสถานะแบบต่อไฟล์เมื่อ visit
FileNodeDowngradeCloneWithRectorประกาศFileNode::classในgetNodeTypes()เพียงเพื่อรีเซ็ตตัวนับตัวแปรชั่วคราวเมื่อเริ่มต้นแต่ละไฟล์ เพื่อให้ชื่อตัวแปรที่สร้างขึ้นไม่ชนกันข้ามไฟล์ - Guard อย่างแม่นยำ แล้วจึงคืนค่า
nullการแปลง clone-with ต้องยืนยันว่าชื่อของการเรียกคือcloneและอาร์กิวเมนต์ที่สองเป็น array literal ก่อนดำเนินการ การclone $objแบบธรรมดาจะไม่เคยไปถึงกฎในฐานะการเรียกฟังก์ชัน และการเรียกแบบสองอาร์กิวเมนต์ที่อาร์กิวเมนต์ที่สองไม่ใช่ array จะถูกปล่อยไว้ตามเดิม - ถอด modifier ที่ target ไม่สามารถแสดงออกได้ เมื่อกฎ trait-constants เปลี่ยน constant ให้เป็น static property กฎจะคงระดับ visibility และเพิ่ม
staticแต่ต้องไม่นำ modifierfinalติดไปด้วย เพราะ property ของ PHP 8.1 ไม่สามารถเป็น final ได้ - รักษากฎให้อยู่ที่ PHPStan Level 10 repository รัน
composer analyseที่ level 10 ทั่วทั้งrector/rulesและscriptsกำหนดชนิดให้ทุก signature และ annotate รูปแบบของอาร์เรย์ กฎที่ไม่ผ่านตัว analyser ถือเป็นข้อบกพร่อง ไม่ใช่ฉบับร่าง
ดูเพิ่มเติม
หัวข้อที่มีชื่อว่า “ดูเพิ่มเติม”- คู่มือนักพัฒนา Backport Builder — สถาปัตยกรรมของ pipeline โมเดล branch และ release artifact ที่อยู่รอบกฎเหล่านี้
- เอกสารอ้างอิง Backport API — surface ที่เผยแพร่ของเครื่องมือ build สำหรับ backport
- การกำหนดค่า Backport — build target และการเลือก downgrade-set
- การแก้ปัญหา Backport — วิธีวินิจฉัยความล้มเหลวในต้นไม้ downgrade ที่สร้างขึ้น
- เอกสาร Rector — เอกสารอ้างอิงต้นทางสำหรับ
AbstractRector,RuleDefinitionและคลาส node ของ AST ที่ใช้ในgetNodeTypes()