تخطَّ إلى المحتوى

كتابة قاعدة خفض مخصّصة في 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 فيه عن أدوات التطوير.

  1. استنسخ مستودع النقل العكسي وثبّت اعتمادياته.
  2. تأكّد من توفّر ⁨Rector⁩ و⁨PHPUnit.⁩
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

يتطلّب المستودع ⁨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⁩ أيًّا من هذه الميزات من تلقاء نفسه. لا تكتب قاعدةً مخصّصة إلّا بعد أن تتأكّد من وجود الثغرة نفسها.

يضيف هذا الإجراء قاعدةً جديدة. يحاكي المثال المستخدم القواعدَ القائمة؛ استبدل به ميزتك الخاصّة.

  1. أنشئ صنف القاعدة في rector/rules/. سَمِّه Downgrade<Feature>Rector وضعه في مساحة الأسماء NextPDF\Backport حتى يلتقطه ربط التحميل التلقائي القائم.
  2. وسِّع Rector\Rector\AbstractRector وعَلِّم الصنف بـ final.
  3. نفِّذ getNodeTypes() بحيث تُعيد أضيق مجموعة من أصناف عُقَد ⁨AST⁩ التي تحتاجها القاعدة. كلّما ضاقت المجموعة قلّ عدد العُقَد التي يزورها ⁨Rector.⁩
  4. نفِّذ refactor(). تحقّق من أنّ العقدة من أحد الأنواع المُعلَنة، واحمِ الصيغة المحدّدة التي تستهدفها بدقّة، وحوِّلها، ثمّ أعِد العقدة الجديدة، أو Stmt[]، أو null لعدم التغيير.
  5. نفِّذ getRuleDefinition() مع نموذج ⁨before/after⁩ واحد من CodeSample لكلّ حالة متمايزة تعالجها القاعدة.
  6. أبقِ الملفّ عند المستوى 10 من ⁨PHPStan⁩: صرِّح بأنواع كلّ مُعامِل وقيمة مُعادة وخاصّية، واستخدم أنواع ⁨PHPDoc⁩ العامّة لوصف أشكال المصفوفات.

قاعدة الرؤية غير المتماثلة هي أصغر مثال كامل. فهي تُزيل رايات تحديد رؤية الإسناد (⁨set⁩) وتتأكّد من بقاء رؤية قراءة أساسية:

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

الحارس في عبارة if الأولى بالغ الأهمّية. عندما لا تملك الخاصّية أيّ راية لرؤية الإسناد، تُعيد القاعدة null وتترك العقدة دون تغيير. أمّا القاعدة التي تُحوِّل دون شرط فستعيد كتابة شِفرة كان يجب أن تتركها كما هي.

لكلّ قاعدة اختبار قائم على التجهيزات يشغّل القاعدة على ملفّات .php.inc ويتحقّق من تطابُق المُخرَج. يأتي إطار التشغيل من Rector\Testing\PHPUnit\AbstractRectorTestCase الخاص بـ ⁨Rector.⁩

حالة الاختبار صغيرة ومتّسقة في القواعد الثلاث القائمة:

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

يشير الاختبار إلى ملفّ إعداد خاصّ بكلّ قاعدة في tests/Rector/config/ يُسجِّل القاعدة قيد الاختبار فقط، بحيث يمرّن كلُّ تجهيز قاعدةً واحدة بمعزل عن غيرها:

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

التجهيز هو ملفّ .php.inc يحتوي على الدخل، وفاصل -----، والمُخرَج المتوقَّع. عندما لا تُجري القاعدة أيّ تغيير، احذف الفاصل والكتلة الثانية. يبدو التجهيز المُحوِّل لقاعدة ثوابت السمات هكذا:

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

لتأليف اختبارات قاعدة جديدة:

  1. أنشئ tests/Rector/Fixtures/Downgrade<Feature>/ وأضف ملفّ .php.inc واحدًا لكلّ حالة.
  2. غطِّ الحالتين المُحوِّلتين بفاصل ----- وحالات التخطّي بلا فاصل، مثل خاصّية بلا رؤية إسناد يجب أن تمرّ دون تغيير.
  3. أضف tests/Rector/config/downgrade_<feature>.php يُسجِّل قاعدتك فقط.
  4. أضف tests/Rector/Downgrade<Feature>RectorTest.php يُنتِج دليل التجهيزات ويشير إلى الإعداد.
  5. شغّل المجموعة.
Terminal window
composer test

يتضمّن المستودع أيضًا RectorRulesBehaviorTest وRectorRulesMetadataTest، اللذين يتحقّقان من السلوك المشترك بين القواعد ويؤكّدان أنّ getRuleDefinition() في كلّ قاعدة سليم البنية. شغّل composer test بالكامل حتى تمرّ قاعدتك الجديدة عبر هذه البوّابات.

لا تصبح القاعدة فعّالة في البناء حتى تُسجِّلها في إعدادات المسار. هناك هدفا بناء، ولكلٍّ منهما إعداده الخاصّ في rector/config/.

  1. افتح rector/config/rector-php81.php وأضف صنف قاعدتك إلى قائمة ->withRules([...]).
  2. إذا كان يجب خفض الميزة أيضًا لبناء نواة ⁨PHP 7.4⁩، فأضف الصنف نفسه إلى rector/config/rector-php74.php.
  3. أضف تعليقًا يُسمّي إصدار ⁨PHP⁩ الذي قدّم الميزة، بما يطابق المُدخَلات القائمة.
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,
]);

يدمج مُنسِّق البناء (scripts/build.php) مستودعات المصدر، ويشغّل ⁨Rector⁩ بهذه الإعدادات، ويعدّل composer.json المُولَّد، ويُجري فحص الصيغة php -l على المُخرَج. تحقّق من قاعدتك باستخدام ⁨PHPStan⁩ والبناء الكامل قبل أن تعتمد عليها.

Terminal window
composer analyse
composer 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. صرِّح بأنواع كلّ توقيع وعلّق أشكال المصفوفات؛ فالقاعدة التي لا تجتاز المُحلِّل عيب وليست مسوّدة.