كتابة قاعدة خفض مخصّصة في Rector
لمحة سريعة
قسم بعنوان «لمحة سريعة»يُخفِض مسار النقل العكسي في NextPDF مصدر PHP 8.4 الخاص بـ nextpdf/nextpdf ليعمل على PHP 8.1، وعلى PHP 7.4 في النواة. ويستخدم مجموعات الخفض المدمجة في Rector لمعظم ميزات اللغة، إلى جانب مجموعة صغيرة من القواعد المخصّصة للميزات التي لا يغطّيها Rector.
يوضّح هذا الدليل كيفية إضافة قاعدة مخصّصة جديدة. ويتّبع البنية نفسها التي تتّبعها القواعد الثلاث الموجودة حاليًا في المستودع: DowngradeAsymmetricVisibilityRector، وDowngradeCloneWithRector، وDowngradeTraitConstantsRector. تقع القواعد الثلاث جميعها في rector/rules/، وهي مُسجَّلة في rector/config/، ولها اختبارات قائمة على التجهيزات (fixtures) في 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($obj, [...])) يمكن أن يظهر في موضعَي return والإسناد كليهما؛ كما يستخدم زيارة FileNode لإعادة ضبط عدّاد خاصّ بكلّ ملف.
القاعدة التي تُعيد null من refactor() تعني “لا تغيير”. والقاعدة التي تُعيد عقدةً تعني الاستبدال. أمّا القاعدة التي تُعيد Stmt[]، أي قائمة من الجُمَل، فتوسّع جملةً واحدة إلى عدّة جُمَل. لذلك يُحوِّل DowngradeCloneWithRector جملةَ return clone($this, [...]); الواحدة إلى إسناد استنساخ، وإسناد خاصّية واحد لكلّ تجاوُز (override)، وreturn أخيرة.
ما الذي تفعله المجموعات المدمجة بالفعل
قسم بعنوان «ما الذي تفعله المجموعات المدمجة بالفعل»يستدعي إعدادا المسار ->withDowngradeSets(php81: true) و->withDowngradeSets(php74: true). وتربط هاتان المجموعتان كلَّ قاعدة خفض مدمجة للهدف. لا توجد القواعد المخصّصة إلّا لسدّ الثغرات: الرؤية غير المتماثلة في PHP 8.4، والاستنساخ مع التعديل في 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/after واحد منCodeSampleلكلّ حالة متمايزة تعالجها القاعدة. - أبقِ الملفّ عند المستوى 10 من PHPStan: صرِّح بأنواع كلّ مُعامِل وقيمة مُعادة وخاصّية، واستخدم أنواع PHPDoc العامّة لوصف أشكال المصفوفات.
قاعدة الرؤية غير المتماثلة هي أصغر مثال كامل. فهي تُزيل رايات تحديد رؤية الإسناد (set) وتتأكّد من بقاء رؤية قراءة أساسية:
<?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 يحتوي على الدخل، وفاصل -----، والمُخرَج المتوقَّع. عندما لا تُجري القاعدة أيّ تغيير، احذف الفاصل والكتلة الثانية. يبدو التجهيز المُحوِّل لقاعدة ثوابت السمات هكذا:
<?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، وهذا سيفشل على خاصّية للقراءة فقط، لذا يجب أن يعملDowngradeReadonlyPropertyRectorللهدف نفسه. - مرِّر سمات (attributes) فارغة عند بناء عقدة استبدال من نوع عقدة مختلف. يبني
DowngradeTraitConstantsRectorعقدةَPropertyمنClassConstويمرّر[]للسمات بدلاً من سمات عقدة المصدر. إذا نقلتَ السمات الأصلية، فستترك مؤشّرorigNodeيشير إلى نوع عقدة خاطئ، ما يُسقِط توكيدًا (assertion) في الطابعة الحافظة للتنسيق. - أعِد ضبط الحالة الخاصّة بكلّ ملفّ عند زيارة
FileNode. يعلنDowngradeCloneWithRectorعنFileNode::classفيgetNodeTypes()فقط لإعادة ضبط عدّاد متغيّراته المؤقّتة عند بداية كلّ ملفّ، بحيث لا تتصادم أسماء المتغيّرات المُولَّدة عبر الملفّات. - احرس بدقّة، ثمّ أعِد
null. يجب أن يتأكّد تحويلُ الاستنساخ مع التعديل من أنّ اسم الاستدعاء هوcloneوأنّ الوسيط الثاني هو حرفية مصفوفة قبل أن يتصرّف؛ فالاستنساخ المجرّدclone $objلا يصل إلى القاعدة قطّ كاستدعاء دالة، والاستدعاء ذو الوسيطين الذي لا يكون وسيطه الثاني مصفوفةً يُترَك كما هو. - جرِّد المُعدِّلات (modifiers) التي لا يستطيع الهدف التعبير عنها. عندما تُحوِّل قاعدة ثوابت السمات ثابتًا إلى خاصّية ساكنة، فإنها تحافظ على الرؤية وتُضيف
static، لكن يجب ألّا تحمل مُعدِّلfinalلأنّ خصائص PHP 8.1 لا يمكن أن تكون نهائية (final). - أبقِ القاعدة عند المستوى 10 من PHPStan. يشغّل المستودع
composer analyseعند المستوى 10 علىrector/rulesوscripts. صرِّح بأنواع كلّ توقيع وعلّق أشكال المصفوفات؛ فالقاعدة التي لا تجتاز المُحلِّل عيب وليست مسوّدة.
انظر أيضًا
قسم بعنوان «انظر أيضًا»- دليل مطوّري أداة بناء النقل العكسي (Backport Builder) — بنية المسار، ونموذج الفروع، وأدوات الإصدار المرتبطة بهذه القواعد.
- مرجع واجهة برمجة تطبيقات النقل العكسي (Backport API) — الواجهة المنشورة لأدوات بناء النقل العكسي.
- إعداد النقل العكسي — أهداف البناء واختيار مجموعة الخفض.
- استكشاف أخطاء النقل العكسي وإصلاحها — كيفية تشخيص الإخفاقات في شجرة خفض مُولَّدة.
- توثيق Rector — المرجع الأساسي لـ
AbstractRector، وRuleDefinition، وأصناف عُقَد AST المستخدَمة فيgetNodeTypes().