الفرق بين المراجعتين لصفحة: «Twig/advanced»

من موسوعة حسوب
1.0: إضافة عنوان الصفحة.
 
طلا ملخص تعديل
 
(9 مراجعات متوسطة بواسطة مستخدمين اثنين آخرين غير معروضة)
سطر 1: سطر 1:
<noinclude>{{DISPLAYTITLE:توسيع Twig}}</noinclude>
<noinclude>{{DISPLAYTITLE:توسيع عمل محرك Twig}}</noinclude>
توسيع Twig يتم بعدة طرائق، إذ تستطيع إضافة وسوم tags إضافية ومرشحات واختبارات وعوامل operators ومتغيرات عامة global variables ودوال أيضًا، بل تستطيع توسيع المحلل parser نفسه بزوار العقد node visitors. لاحظ أن أول جزء من هذه الصفحة يشرح كيفية توسيع عمل محرك Twig، فإذا أردت إعادة استخدام ما تفعله في مشاريع مختلفة أو كنت تريد مشاركتها مع غيرك فيجب أن تنشئ توسيعًا بنفسك كما هو موضح أدناه.
 
وانتبه إلى أن توسيع Twig من غير إنشاء توسعة جديدة extension تجعل Twig غير قادر على إعادة تصريف recompile قوالبك عند تحديث شيفرة [[PHP]]، ويجب أن تعطل تخزين القوالب المؤقت template caching أو تحزِّم شيفرتك في توسيعة كي ترى تغييراتك في الوقت الحقيقي، كما سيلي بيانه لاحقًا في هذه الصفحة.
 
__toc__
 
يجب كذلك أن تفهم أولًا الاختلافات بين نقاط التوسعة extension points المختلفة ومتى يجب استخدام كل منها، قبل توسيع Twig نفسه، فاعلم أن Twig به بنيتين لغويتين أساسيتين:
 
* <code><nowiki>{{ }}</nowiki></code>: تُستخدم لطباعة نتيجة تقييم التعبير.
* <code>{% %}</code>: تُستخدم لتنفيذ التعليمات.
 
انظر كيفية تنفيذ مولد النص التلقائي lorem ipsum -الذي يحتاج أن يعرف عدد الكلمات التي يجب توليدها- كي تفهم كيف يكشف Twig كثيرًا من نقاط التوسعة، استخدم وسم <code>lipsum</code>:<syntaxhighlight lang="twig">
{% lipsum 40 %}
 
</syntaxhighlight>هذا وإن كان يعمل إلا أن استخدام وسمًا لـ <code>lipsum</code> ليس الخيار الأمثل لثلاثة أسباب رئيسية:
 
* <code>lipsum</code> ليس بنية لغوية.
* الوسم له مخرجات.
* الوسم ليس مرنًا إذ لا تستطيع استخدامه في تعبير:
<syntaxhighlight lang="twig">
{{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}
 
</syntaxhighlight>ولا تحتاج إلى إنشاء وسوم إلا نادرًا، وهذا أمر حسن لأنها أعقد نقاط التوسعة. لنستخدم الآن مرشح <code>lipsum</code>: <syntaxhighlight lang="twig">
{{ 40|lipsum }}
 
</syntaxhighlight>وهذا يعمل أيضًا لكن يجب أن يحول المرشحُ القيمةَ الممرَّرة إلى شيء آخر، وهنا نستخدم القيمة للإشارة إلى عدد الكلمات التي يجب توليدها، لذا فإن <code>40</code> هي وسيط argument للمرشح، وليست القيمة التي نريد تحويلها. لنستخدم الآن دالة <code>lipsum</code>:<syntaxhighlight lang="twig">
{{ lipsum(40) }}
 
</syntaxhighlight>ولهذا المثال خاصة فإن نقطة التوسعة التي ستُستخدم هي إنشاء الدالة، وتستطيع استخدامها في أي مكان يقبل تعبيرًا:<syntaxhighlight lang="twig">
{{ 'some text' ~ lipsum(40) ~ 'some more text' }}
 
{% set lipsum = lipsum(40) %}
</syntaxhighlight>وأخيرًا تستطيع استخدام كائنًا عامًا global object مع تابع قادر على إنشاء نص لوريم إبسوم:<syntaxhighlight lang="twig">
{{ text.lipsum(40) }}
 
</syntaxhighlight>وكقاعدة عامة، استخدم الدوال في حالة المزايا التي يكثر استخدامها، والكائنات العامة لأي شيء آخر، واعتبر بالجدول التالي عند توسيع Twig:
{| class="wikitable"
|+طرق توسيع Twig وحالات استخدامها
!الطريقة
!صعوبة التنفيذ
!التواتر
!متى
|-
|شيفرة جامعة macro
|بسيط
|متكرر
|إنشاء المحتوى
|-
|عام global
|بسيط
|متكرر
|كائن مساعد
|-
|دالة function
|بسيط
|متكرر
|تَحوُّل القيمة
|-
|وسم tag
|معقد
|نادر
|بنية خاصة للغة Domain Specific Language
|-
|اختبار test
|بسيط
|نادر
|إجراء بولياني
|-
|عامل operator
|بسيط
|نادر
|تَحوُّل القيمة
|}
 
== المتغيرات العامة Global Variables ==
المتغير العام يشبه أي متغير قالب آخر إلا أنه متاح في جميع القوالب والشيفرات الجامعة:<syntaxhighlight lang="php">
$twig = new \Twig\Environment($loader);
$twig->addGlobal('text', new Text());
</syntaxhighlight>تستطيع حينها استخدام متغير <code>text</code> في أي مكان داخل القالب:<syntaxhighlight lang="twig">
{{ text.lipsum(40) }}
 
</syntaxhighlight>
 
== المرشحات ==
إنشاء المرشح يكون بربط اسم مع نوع البيانات "[[PHP/callable|callable]]" من [[PHP]]:<syntaxhighlight lang="php">
// دالة مجهولة
$filter = new \Twig\TwigFilter('rot13', function ($string) {
    return str_rot13($string);
});
 
// بسيطة PHP أو دالة
$filter = new \Twig\TwigFilter('rot13', 'str_rot13');
 
// class static method أو تابع صنف ساكن
$filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);
$filter = new \Twig\TwigFilter('rot13', 'SomeClass::rot13Filter');
 
// أو تابع صنف
$filter = new \Twig\TwigFilter('rot13', [$this, 'rot13Filter']);
// هذا الأخير يحتاج إلى تطبيق في وقت التشغيل، انظر أدناه للتوضيح.
$filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);
</syntaxhighlight>يكون أول وسيط ممرَّر إلى منشئ ‎<code>\Twig\TwigFilter</code> هو اسم المرشح الذي ستستخدمه في القوالب والثاني هو نوع البيانات "[[PHP/callable|callable]]" المرتبط به. ثم بعد ذلك، نضيف المرشح إلى بيئة Twig: <syntaxhighlight lang="php">
$twig = new \Twig\Environment($loader);
$twig->addFilter($filter);
</syntaxhighlight>انظر الآن كيف تستخدمه في قالب:<syntaxhighlight lang="twig">
{{ 'Twig'|rot13 }}
 
{# Gjvt سيُخرج #}
</syntaxhighlight>يستقبل نوع البيانات "[[PHP/callable|callable]]" (ردود النداء) الجانب الأيسر من المرشح قبل الأنبوب <code>|</code> كوسيط أول عند استدعاء Twig له، وتمرَّر وسائط إضافية إلى المرشح داخل أقواس <code>()</code>. فمثلًا تُصرَّف الشيفرة التالية:<syntaxhighlight lang="twig">
{{ 'TWIG'|lower }}
{{ now|date('d/m/Y') }}
</syntaxhighlight>لتكون كما يلي بعد التصريف:<syntaxhighlight lang="php">
<?php echo strtolower('TWIG') ?>
<?php echo twig_date_format_filter($now, 'd/m/Y') ?>
</syntaxhighlight>يأخذ الصنف ‎<code>\Twig\TwigFilter</code> مصفوفة من الخيارات كآخر وسيط له:<syntaxhighlight lang="php">
$filter = new \Twig\TwigFilter('rot13', 'str_rot13', $options);
 
</syntaxhighlight>
 
=== المرشحات الواعية للبيئة ===
إذا أردت الوصول إلى نسخة البيئة الحالية في مرشحك فاجعل الخيار <code>needs_environment</code> على <code>true</code>، وسيمرر Twig البيئة الحالية كأول وسيط إلى استدعاء المرشح:<syntaxhighlight lang="php">
$filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $string) {
    // اجلب مجموعة المحارف الحالية للنسخة.
    $charset = $env->getCharset();
 
    return str_rot13($string);
}, ['needs_environment' => true]);
</syntaxhighlight>
 
=== المرشحات الواعية للسياق ===
إذا أردت الوصول إلى السياق الحالي في مرشحك فاجعل الخيار <code>needs_context</code> على <code>true</code>، وسيمرر Twig السياق الحالي كأول وسيط إلى استدعاء المرشح أو الوسيط الثاني إذا كان <code>needs_environment</code> على <code>true</code> أيضًا:<syntaxhighlight lang="php">
$filter = new \Twig\TwigFilter('rot13', function ($context, $string) {
    // ...
}, ['needs_context' => true]);
 
$filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $context, $string) {
    // ...
}, ['needs_context' => true, 'needs_environment' => true]);
</syntaxhighlight>
 
=== التهريب التلقائي Automatic Escaping ===
إذا كان التهريب التلقائي مفعلًا فقد يُهرَّب خرج المرشح قبل الطباعة، وإن كان مرشحك يتصرف كمهرِّب أو يخرج شيفرة HTML أو جافاسكربت فربما تريد طباعة الخرج الخام، فاضبط خيار <code>is_safe</code> في مثل تلك الحالة:<syntaxhighlight lang="php">
$filter = new \Twig\TwigFilter('nl2br', 'nl2br', ['is_safe' => ['html']]);
</syntaxhighlight>قد تحتاج بعض المرشحات إلى التعامل مع مدخلات مهرَّبة سلفًا أو آمنة، كما في حالة إضافة وسوم HTML آمنة إلى خرج غير آمن، ففي مثل تلك الحالة، اضبط خيار <code>pre_escape</code> ليهرب بيانات الدخل قبل تشغيلها عبر المرشح الخاص بك:<syntaxhighlight lang="php">
$filter = new \Twig\TwigFilter('somefilter', 'somefilter', ['pre_escape' => 'html', 'is_safe' => ['html']]);
</syntaxhighlight>
 
=== المرشحات المتغيرة Variadic Filters ===
إذا وجب على مرشح أن يقبل عددًا عشوائيًا من الوسائط، فاجعل خيار <code>is_variadic</code> على <code>true</code>، كي يمرِّر Twig الوسائط الإضافية كوسيط أخير في هيئة مصفوفة إلى استدعاء المرشح:<syntaxhighlight lang="php">
$filter = new \Twig\TwigFilter('thumbnail', function ($file, array $options = []) {
    // ...
}, ['is_variadic' => true]);
</syntaxhighlight>لكن انتبه إلى أن [[Twig/templates|الوسائط المسمَّاة named arguments]] التي تمرَّر إلى مرشح متغير لا يمكن التحقق من صلاحيتها بما أنها ستؤول تلقائيًا إلى مصفوفة الخيارات option array.
 
=== المرشحات الديناميكية ===
المرشح الديناميكي هو مرشح يحتوي اسمه على المحرف الخاص <code>*</code>، وهذا المحرف مطابق لأي سلسلة نصية:<syntaxhighlight lang="php">
$filter = new \Twig\TwigFilter('*_path', function ($name, $arguments) {
    // ...
});
</syntaxhighlight>المرشحات التالية مطابقة للمرشح الديناميكي المعرَّف أعلاه:
 
* <code>product_path</code>
* <code>category_path</code>
 
يستطيع المرشح الديناميكي أن يعرِّف أكثر من جزء ليكون ديناميكيًا أيضًا:<syntaxhighlight lang="php">
$filter = new \Twig\TwigFilter('*_path_*', function ($name, $suffix, $arguments) {    // ...});
</syntaxhighlight>يستقبل المرشح كل قيم الجزء الديناميكي قبل وسائط المرشح العادي، لكن بعد البيئة والسياق، فمثلًا، إذا استدعينا ‎<code>'foo'|a_path_b()</code>‎ فستُمرَّر الوسائط <code>('a', 'b', 'foo')</code> إلى المرشح.
 
=== المرشحات المهملة Deprecated Filters ===
تستطيع تحديد المرشح ليكون مهملًا من خلال ضبط الخيار <code>deprecated</code> على <code>true</code>، كما تستطيع تحديد مرشح بديل يكون بديلًا للمهمل إذا رأيت ذلك:<syntaxhighlight lang="php">
$filter = new \Twig\TwigFilter('obsolete', function () {
    // ...
}, ['deprecated' => true, 'alternative' => 'new_one']);
</syntaxhighlight>عند إهمال المرشح فإن Twig يرسل إشعار إهمال عند تصريف قالب يستخدم ذلك المرشح، انظر صفحة [[Twig/recipes|الوصفات]] للمزيد.
 
== الدوال ==
تعرَّف الدوال بنفس طريقة تعريف المرشحات، لكن تحتاج إلى إنشاء نسخة من ‎<code>\Twig\TwigFunction</code>:<syntaxhighlight lang="php">
$twig = new \Twig\Environment($loader);
$function = new \Twig\TwigFunction('function_name', function () {
    // ...
});
$twig->addFunction($function);
</syntaxhighlight>تدعم الدوال نفس المزايا التي تدعمها المرشحات ما عدا خياري <code>pre_escape</code> و <code>preserves_safety</code>.
 
== الاختبارات ==
تعَّرف الاختبارات بنفس طريقة تعريف الدوال والمرشحات، لكن تحتاج إلى إنشاء نسخة من ‎<code>\Twig\TwigTest</code>:<syntaxhighlight lang="php">
$twig = new \Twig\Environment($loader);
$test = new \Twig\TwigTest('test_name', function () {
    // ...
});
$twig->addTest($test);
</syntaxhighlight>تسمح لك الاختبارات بإنشاء منطق يختص بتطبيق ما من أجل تقييم الشرطيات البوليانية boolean conditions. مثلًا، لتنشئ اختبار Twig يتحقق إن كانت الكائنات حمراء red:<syntaxhighlight lang="php">
$twig = new \Twig\Environment($loader);
$test = new \Twig\TwigTest('red', function ($value) {
    if (isset($value->color) && $value->color == 'red') {
        return true;
    }
    if (isset($value->paint) && $value->paint == 'red') {
        return true;
    }
    return false;
});
$twig->addTest($test);
</syntaxhighlight>يجب أن تعيد دوال الاختبار إما <code>true</code> أو <code>false</code>.
 
استخدم الخيار <code>node_class</code> عند إنشاء الاختبارات من أجل توفير تجميعة مخصصة للاختبار، وذلك ينفعك إن كان يمكن تصريف اختبارك داخل أنوع البيانات الأولية لـ PHP، وتستخدمه اختبارات عديدة مضمنة في Twig:<syntaxhighlight lang="php">
namespace App;
 
use Twig\Environment;
use Twig\Node\Expression\TestExpression;
use Twig\TwigTest;
 
$twig = new Environment($loader);
$test = new TwigTest(
    'odd',
    null,
    ['node_class' => OddTestExpression::class]);
$twig->addTest($test);
 
class OddTestExpression extends TestExpression
{
    public function compile(\Twig\Compiler $compiler)
    {
        $compiler
            ->raw('(')
            ->subcompile($this->getNode('node'))
            ->raw(' % 2 != 0')
            ->raw(')')
        ;
    }
}
</syntaxhighlight>يوضح المثال أعلاه كيف تنشئ اختبارات تستخدم صنف العقدة node class الذي يكون له وصول إلى عقدة فرعية اسمها <code>node</code>، تحتوي على القيمة التي تُختبر.<syntaxhighlight lang="twig">
{% if my_value is odd %}
</syntaxhighlight>إذا استُخدم مرشح <code>odd</code> في شيفرة مثل التي أعلاه فإن عقدة <code>node</code> ستحتوي على تعبير لـ <code>my_value</code>، ويكون للاختبارات المبنية على عقد وصول أيضًا إلى عقدة <code>arguments</code>، وستحتوي تلك العقدة على وسائط أخرى عديدة تم توفيرها لاختبارك.
 
وإذا أردت تمرير عددًا متغيرًا من الوسائط الموضعية positional أو المسماة named إلى الاختبار فاضبط خيار <code>is_variadic</code> على <code>true</code>، إذ أن الاختبارات تدعم الأسماء الديناميكية انظر المرشحات الديناميكية للبنية اللغوية Syntax.
 
== الوسوم ==
إحدى المزايا المثيرة في محرك قوالب مثل Twig هو إمكانية تعريف بنيات لغوية جديدة، وهي إحدى أعقد الإمكانيات التي تتعرض لها إذا أردت فهم كيفية عمل Twig، لكننا لن نحتاج إلى وسم في أغلب الأحيان:
 
* إذا كان وسمك يولد بعض المخرجات فاستخدم دالة.
* إذا كان وسمك يعدل بعض المحتوى ويعيده فاستخدم مرشحًا.
 
مثلًا، إذا أردت إنشاء وسم يحول نصًا بصيغة مارك داون إلى HTML، فأنشئ مرشح <code>markdown</code> بدلًا من إنشاء وسم:<syntaxhighlight lang="twig">
{{ '**markdown** text'|markdown }}
</syntaxhighlight>إذا أردت استخدام هذا المرشح على نصوص كثيرة فغلفه بوسم [[Twig/apply|<code>apply</code>]]:<syntaxhighlight lang="twig">
{% apply markdown %}
Title
=====
 
Much better than creating a tag as you can **compose** filters.
{% endapply %}
</syntaxhighlight>إذا لم يكن وسمك يخرج أي شيء لكنه موجود بسبب أثر جانبي فأنشئ دالة لا تعيد شيئًا واستدعها من خلال وسم <code>[[Twig/filter|filter]]</code>، فمثلًا إذا أردت إنشاء وسم يسجل النصوص فأنشئ دالة <code>log</code> واستدعها من خلال وسم <code>[[Twig/do|do]]</code>:<syntaxhighlight lang="twig">
{% do log('Log some things') %}
</syntaxhighlight>والآن لننشئ وسمًا لبُنيةٍ لغوية جديدة وليكن <code>[[Twig/set|set]]</code> الذي يسمح بتعريف متغيرات بسيطة من داخل القالب، ويمكن استخدام ذلك الوسم كما يلي:<syntaxhighlight lang="twig">
{% set name = "value" %}
 
{{ name }}
 
{# should output value #}
</syntaxhighlight>لاحظ أن وسم [[Twig/set|<code>set</code>]] جزء من توسيع النواة <code>core</code> وعليه سيكون متاحًا دائمًا، والنسخة المضمنة منه أقوى وتدعم إسنادات متعددة افتراضيًا. ستحتاج إلى ثلاثة خطوات من أجل تعريف وسم جديد:
 
* تعريف صنف محلل للوحدات token parser class مسؤول عن تحليل شيفرة القالب.
* تعريف صنف عقدة يكون مسؤولًا عن تحويل الشيفرة المحلَّلة إلى PHP.
* تسجيل الوسم.
 
=== تسجيل وسم جديد ===
أضف وسمًا من خلال استدعاء التابع <code>addTokenParser</code> على نسخة ‎<code>\Twig\Environment</code>: <syntaxhighlight lang="twig">
$twig = new \Twig\Environment($loader);
$twig->addTokenParser(new Project_Set_TokenParser());
</syntaxhighlight>
 
=== تعريف محلل مفتاح Token Parser  ===
لننظر الآن في الشيفرة الحقيقية لهذا الصنف:<syntaxhighlight lang="php">
class Project_Set_TokenParser extends \Twig\TokenParser\AbstractTokenParser
{
    public function parse(\Twig\Token $token)
    {
        $parser = $this->parser;
        $stream = $parser->getStream();
 
        $name = $stream->expect(\Twig\Token::NAME_TYPE)->getValue();
        $stream->expect(\Twig\Token::OPERATOR_TYPE, '=');
        $value = $parser->getExpressionParser()->parseExpression();
        $stream->expect(\Twig\Token::BLOCK_END_TYPE);
 
        return new Project_Set_Node($name, $value, $token->getLine(), $this->getTag());
    }
 
    public function getTag()
    {
        return 'set';
    }
}
</syntaxhighlight>يجب أن يعيد التابع <code>getTag()‎</code> الوسم الذي نريد تحليله، وهو [[Twig/set|<code>set</code>]] هنا، ويُستدعى التابع <code>parse()‎</code> كلما وجد وسم <code>[[Twig/set|set]]</code>، ويجب أن يعيد نسخة من ‎<code>\Twig\Node\Node</code> تمثل العقدة (ستجد شرح إنشاء استدعاءات <code>Project_Set_Node</code> لاحقًا في الصفحة).
 
وتُبسَّط عملية التحليل بفضل مجموعة من التوابع التي يمكنك استدعاءها من مجرى مفتاح التشفير token stream أي ‎<code>$this->parser->getStream()‎</code>:
 
* <code>getCurrent()‎</code>: تجلب المفتاح token الحالي في المجرى.
 
* <code>next()‎</code>: تتحرك إلى المفتاح التالي في المجرى لكن تعيد المفتاح القديم.
* <code>test($type)</code> أو  <code>test($value)</code> أو <code>test($type, $value)</code>: تحدد ما إذا كان المفتاح الحالي من نوع أو قيمة بعينها أو كليهما، وقد تكون القيمة مصفوفة من عدة قيم ممكنة.
* <code>expect($type[, $value[, $message]])</code>: إذا لم يكن المفتاح الحالي من النوع المعطى أو القيمة المعطاة فيُرفع خطأ لغوي syntax error، وإلا إذا كان النوع صحيحًا وكذلك القيمة فيعاد المفتاح وينتقل المجرى إلى الوحدة التالية.
* <code>look()‎</code>: ينظر إلى الوحدة التالية من غير استهلاكه.
* يتم تحليل التعابير من خلال استدعاء  <code>parseExpression()‎</code> كما فعلنا في وسم <code>set</code>.
 
تستطيع تعلم جميع التفاصيل التي في عملية التحليل بقراءة أصناف <code>TokenParser</code> الموجودة فعليًا.
 
=== تعريف عقدة ===
إن الصنف  <code>Project_Set_Node</code> نفسه قصير نوعًا ما:<syntaxhighlight lang="php">
class Project_Set_Node extends \Twig\Node\Node
{
    public function __construct($name, \Twig\Node\Expression\AbstractExpression $value, $line, $tag = null)
    {
        parent::__construct(['value' => $value], ['name' => $name], $line, $tag);
    }
 
    public function compile(\Twig\Compiler $compiler)
    {
        $compiler
            ->addDebugInfo($this)
            ->write('$context[\''.$this->getAttribute('name').'\'] = ')
            ->subcompile($this->getNode('value'))
            ->raw(";\n")
        ;
    }
}
</syntaxhighlight>ينفذ المصرِّف واجهة سلسلة ويوفر توابع تساعد المطور على توليد شيفرات PHP جميلة وسهلة القراءة:
 
* <code>subcompile()‎</code>: يصرِّف العقدة.
* <code>raw()‎</code>: يكتب السلسلة النصية كما هي.
* <code>write()‎</code>: يكتب السلسلة النصية المعطاة بإضافة إزاحة في بداية كل سطر.
* <code>string()‎</code>: يكتب سلسلة نصية بين علامتي اقتباس.
* <code>repr()‎</code>: يكتب تمثيل PHP لقيمة معطاة، انظر ‎<code>\Twig\Node\ForNode</code> لمثال على استخدامه.
* <code>addDebugInfo()‎</code>: يضيف السطر المتعلق بالعقدة الحالية من ملف القالب الأصلي كتعليق.
* <code>indent()‎</code>: يزيح الشيفرة المولَّدة، انظر ‎<code>\Twig\Node\BlockNode</code> لمثال على الاستخدام.
* <code>outdent()‎</code>: يزيح الشيفرة المولَّدة خارجيًا outdent ، انظر  ‎<code>\Twig\Node\BlockNode</code> لمثال على الاستخدام.
 
== إنشاء توسعة Extension  ==
إن ما يجعلك تنشئ توسعة هو نقل الشيفرة المستخدمة بكثرة إلى صنف قابل لإعادة استخدامه مثل إضافة دعم للدُوَلية مثلًا internationalization، بأن يكون التطبيق مستخدمًا في عدة دول بعدة لغات مختلفة. ويستطيع التوسيع أن يعرِّف الوسوم والمرشحات والاختبارات والعوامل والدوال وزوار العقد node visitors .
 
ومن المفيد غالبًا أن تنشئ توسعة واحدة لمشروعك لاستضافة جميع الوسوم المحددة والمرشحات التي تريد إضافتها إلى Twig. كذلك، عند تحزيم شيفرتك في توسيع فإن Twig من الذكاء بحيث يعيد تصريف قوالبك كلما أجريت تغييرًا فيه، إذا كان <code>auto_reload</code> مفعلًا.
 
التوسعة Extension هي صنف يطبق الواجهة التالية:<syntaxhighlight lang="php">
interface \Twig\Extension\ExtensionInterface
{
    /**
    * يعيد نسخ محلل الرموز لإضافته إلى القائمة الحالية
    *
    * @return \Twig\TokenParser\TokenParserInterface[]
    */
    public function getTokenParsers();
 
    /**
    * يعيد نسخ زائد العقدة لإضافتها إلى القائمة الحالية
    *
    * @return \Twig\NodeVisitor\NodeVisitorInterface[]
    */
    public function getNodeVisitors();
 
    /**
    * يعيد قائمة من الفلاتر لإضافتها إلى القائمة الحالية
    *
    * @return \Twig\TwigFilter[]
    */
    public function getFilters();
 
    /**
    * يعيد قائمة من الاختبارات لإضافتها إلى القائمة الحالية
    *
    * @return \Twig\TwigTest[]
    */
    public function getTests();
 
    /**
    * يعيد قائمة من الدوال لإضافتها إلى القائمة الحالية
    *
    * @return \Twig\TwigFunction[]
    */
    public function getFunctions();
 
    /**
    * يعيد قائمة من العوامل لإضافتها إلى القائمة الحالية.
    * Returns a list of operators to add to the existing list.
    *
    * @return array<array> First array of unary operators, second array of binary operators
    */
    public function getOperators();
}
</syntaxhighlight>إذا أردت الحفاظ على صنف التوسيع الخاص بك رشيقًا فاجعله يرث من الصنف المضمن ‎<code>\Twig\Extension\AbstractExtension</code> بدلًا من تطبيق الواجهة بما أنه يوفر تطبيقات فارغة لجميع التوابع:<syntaxhighlight lang="php">
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
}
</syntaxhighlight>لا تفعل هذه التوسعة أي شيء إلى الآن، وسنخصصها فيما يلي من الشرح، وتستطيع حفظها في أي مكان في نظام الملفات بما أن جميع التوسعات يجب أن تُسجل صراحة لتكون متاحة في قوالبك، كما تستطيع تسجيل توسيع باستخدام التابع <code>addExtension()‎</code> على الكائن الرئيسي <code>Enivronment</code> الخاص بك: <syntaxhighlight lang="php">
$twig = new \Twig\Environment($loader);
$twig->addExtension(new Project_Twig_Extension());
</syntaxhighlight>تستطيع النظر في توسعات النواة <code>core</code> الخاصة بـ Twig إذ هي أمثلة ممتازة على كيفية عمل التوسعات.
 
=== المتغيرات العامة ===
يمكن تسجيل المتغيرات العامة في توسعة من خلال التابع <code>getGlobals()‎</code>:<syntaxhighlight lang="php">
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface
{
    public function getGlobals(): array
    {
        return [
            'text' => new Text(),
        ];
    }
 
    // ...
}
</syntaxhighlight>
 
=== الدوال ===
يمكن تسجيل الدوال في توسعة من خلال التابع <code>getFunctions()‎</code> :<syntaxhighlight lang="php">
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getFunctions()
    {
        return [
            new \Twig\TwigFunction('lipsum', 'generate_lipsum'),
        ];
    }
 
    // ...
}
</syntaxhighlight>
 
=== المرشحات ===
إذا أردت إضافة مرشح إلى توسعة فستحتاج إلى إعادة تعريف التابع <code>getFilters()‎</code>، فيجب أن يعيد هذا التابع مصفوفة من المرشحات لإضافتها إلى بيئة Twig:<syntaxhighlight lang="php">
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getFilters()
    {
        return [
            new \Twig\TwigFilter('rot13', 'str_rot13'),
        ];
    }
 
    // ...
}
</syntaxhighlight>
 
=== الوسوم ===
يمكن إضافة الوسوم إلى توسعة بإعادة تعريف override التابع <code>getTokenParsers()‎</code>، فيجب أن يعيد مصفوفةً من الوسوم لإضافتها إلى بيئة Twig:<syntaxhighlight lang="php">
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getTokenParsers()
    {
        return [new Project_Set_TokenParser()];
    }
 
    // ...
}
</syntaxhighlight>أضفنا في الشيفرة السابقة وسمًا واحدًا جديدًا، عرَّفه الصنف <code>Project_Set_TokenParser</code>، وهذا الصنف مسؤول عن تحليل الوسم وتصريفه إلى [[PHP/Array|PHP]].
 
=== العوامل Operators  ===
يسمح لك التابع  <code>getOperators()‎</code> بإضافة عوامل جديدة، انظر الشيفرة التالية كمثال على كيفية إضافة العوامل <code>!</code> و <code>||</code> و <code>&&</code>:<syntaxhighlight lang="php">
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getOperators()
    {
        return [
            [
                '!' => ['precedence' => 50, 'class' => \Twig\Node\Expression\Unary\NotUnary::class],
            ],
            [
                '||' => ['precedence' => 10, 'class' => \Twig\Node\Expression\Binary\OrBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
                '&&' => ['precedence' => 15, 'class' => \Twig\Node\Expression\Binary\AndBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
            ],
        ];
    }
 
    // ...
}
</syntaxhighlight>
 
=== الاختبارات ===
يسمح لك التابع  <code>getTests()‎</code> بإضافة دوال اختبار جديدة:<syntaxhighlight lang="php">
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getTests()
    {
        return [
            new \Twig\TwigTest('even', 'twig_test_even'),
        ];
    }
 
    // ...
}
</syntaxhighlight>
 
=== موازنة بين التعريف ووقت التشغيل ===
يمكن تعريف تنفيذات وقت التشغيل runtime implementations لمرشحات Twig ودواله واختباراته كأي نوع بيانات "[[PHP/callable|callable]]" في PHP:
 
* '''الدوال/التوابع الساكنة''': بسيطة في تطبيقها، وسريعة -تستخدمها جميع توسعات <code>core</code> في Twig، لكن من الصعب أن يعتمد وقت التشغيل runtime على كائنات خارجية.
* '''الإغلاقات closures''' : بسيطة في تنفيذها.
* '''توابع الكائن''': أكثر مرونة وتحتاجها إن كانت شيفرة وقت التشغيل لديك تعتمد على كائنات خارجية.
 
إن أبسط طريقة لاستخدام التوابع هي تعريفها على توسعة نفسها:<syntaxhighlight lang="php">
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    private $rot13Provider;
 
    public function __construct($rot13Provider)
    {
        $this->rot13Provider = $rot13Provider;
    }
 
    public function getFunctions()
    {
        return [
            new \Twig\TwigFunction('rot13', [$this, 'rot13']),
        ];
    }
 
    public function rot13($value)
    {
        return $this->rot13Provider->rot13($value);
    }
}
</syntaxhighlight>هذه الطريقة على بساطتها إلا أنه لا يُنصح بها لأنها تجعل تصريف القالب يعتمد على اعتماديات وقت التشغيل حتى لو لم تكن ثمة حاجة إليها.
 
تستطيع فصل تعريفات توسعة من تنفيذات وقت تشغيلها بتسجيل نسخة من ‎<code>\Twig\RuntimeLoader\RuntimeLoaderInterface</code> على البيئة التي تعرف كيفية بدء أصناف وقت التشغيل تلك، ويجب أن تكون أصناف وقت التشغيل قابلة للتحميل التلقائي:<syntaxhighlight lang="php">
class RuntimeLoader implements \Twig\RuntimeLoader\RuntimeLoaderInterface
{
    public function load($class)
    {
        // $class طبق المنطق لإنشاء نسخة من
        // وأدخل اعتمادياتها.
        // وسيعني ذلك في الغالب أن تستخدم حاوية حقن الاعتماديات
        // dependency injection container أو
        if ('Project_Twig_RuntimeExtension' === $class) {
            return new $class(new Rot13Provider());
        } else {
            // ...
        }
    }
}
 
$twig->addRuntimeLoader(new RuntimeLoader());
</syntaxhighlight>لاحظ أن Twig يأتي بمحمِّل وقت تشغيل ‎<code>\Twig\RuntimeLoader\ContainerRuntimeLoader</code> متوافق مع PSR-11، وصار من الممكن أن تنقل منطق وقت التشغيل إلى صنف <code>Project_Twig_RuntimeExtension</code> جديد وتستخدمه مباشرة في التوسيع:<syntaxhighlight lang="php">
class Project_Twig_RuntimeExtension
{
    private $rot13Provider;
 
    public function __construct($rot13Provider)
    {
        $this->rot13Provider = $rot13Provider;
    }
 
    public function rot13($value)
    {
        return $this->rot13Provider->rot13($value);
    }
}
 
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getFunctions()
    {
        return [
            new \Twig\TwigFunction('rot13', ['Project_Twig_RuntimeExtension', 'rot13']),
            // أو
            new \Twig\TwigFunction('rot13', 'Project_Twig_RuntimeExtension::rot13'),
        ];
    }
}
</syntaxhighlight>
 
== اختبار توسعة ==
 
=== الاختبارات الدالية  ===
تستطيع إنشاء اختبارات دالّية Functional Tests للتوسعات من خلال إنشاء بنية الملف التالية في مجلد اختبارك:<syntaxhighlight lang="php">
Fixtures/
    filters/
        foo.test
        bar.test
    functions/
        foo.test
        bar.test
    tags/
        foo.test
        bar.test
IntegrationTest.php
</syntaxhighlight>ويجب أن يبدو ملف  <code>IntegrationTest.php</code> كما يلي:<syntaxhighlight lang="php">
use Twig\Test\IntegrationTestCase;
 
class Project_Tests_IntegrationTest extends IntegrationTestCase
{
    public function getExtensions()
    {
        return [
            new Project_Twig_Extension1(),
            new Project_Twig_Extension2(),
        ];
    }
 
    public function getFixturesDir()
    {
        return __DIR__.'/Fixtures/';
    }
}
</syntaxhighlight>تستطيع العثور على أمثلة لواجهات الاختبار Fixtures داخل مستودع Twig، داخل مجلد [https://github.com/twigphp/Twig/tree/3.x/tests/Fixtures tests/Twig/Fixtures].
 
=== اختبارات العقد ===
قد يكون اختبار زوار العقد معقدًا، لذلك اجلب حالات اختبارك عبر الوراثة من ‎<code>\Twig\Test\NodeTestCase</code>، وستجد أمثلة في مستودع Twig، داخل مجلد [https://github.com/twigphp/Twig/tree/3.x/tests/Node tests/Twig/Node].
 
== انظر أيضًا ==
 
* [[Twig/templates|Twig لمصممي القوالب]]
* [[Twig/api|Twig للمطورين]]
* [[Twig/internals|المكونات الداخلية لمحرك القوالب Twig]]
 
== المصادر ==
 
* [https://twig.symfony.com/doc/3.x/advanced.html#environment-aware-filters صفحة Extending Twig من توثيق Twig الرسمي]
[[تصنيف:Twig]]
[[تصنيف:Twig Intro]]

المراجعة الحالية بتاريخ 10:55، 7 أكتوبر 2022

توسيع Twig يتم بعدة طرائق، إذ تستطيع إضافة وسوم tags إضافية ومرشحات واختبارات وعوامل operators ومتغيرات عامة global variables ودوال أيضًا، بل تستطيع توسيع المحلل parser نفسه بزوار العقد node visitors. لاحظ أن أول جزء من هذه الصفحة يشرح كيفية توسيع عمل محرك Twig، فإذا أردت إعادة استخدام ما تفعله في مشاريع مختلفة أو كنت تريد مشاركتها مع غيرك فيجب أن تنشئ توسيعًا بنفسك كما هو موضح أدناه.

وانتبه إلى أن توسيع Twig من غير إنشاء توسعة جديدة extension تجعل Twig غير قادر على إعادة تصريف recompile قوالبك عند تحديث شيفرة PHP، ويجب أن تعطل تخزين القوالب المؤقت template caching أو تحزِّم شيفرتك في توسيعة كي ترى تغييراتك في الوقت الحقيقي، كما سيلي بيانه لاحقًا في هذه الصفحة.

يجب كذلك أن تفهم أولًا الاختلافات بين نقاط التوسعة extension points المختلفة ومتى يجب استخدام كل منها، قبل توسيع Twig نفسه، فاعلم أن Twig به بنيتين لغويتين أساسيتين:

  • {{ }}: تُستخدم لطباعة نتيجة تقييم التعبير.
  • {% %}: تُستخدم لتنفيذ التعليمات.

انظر كيفية تنفيذ مولد النص التلقائي lorem ipsum -الذي يحتاج أن يعرف عدد الكلمات التي يجب توليدها- كي تفهم كيف يكشف Twig كثيرًا من نقاط التوسعة، استخدم وسم lipsum:

{% lipsum 40 %}

هذا وإن كان يعمل إلا أن استخدام وسمًا لـ lipsum ليس الخيار الأمثل لثلاثة أسباب رئيسية:

  • lipsum ليس بنية لغوية.
  • الوسم له مخرجات.
  • الوسم ليس مرنًا إذ لا تستطيع استخدامه في تعبير:
{{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}

ولا تحتاج إلى إنشاء وسوم إلا نادرًا، وهذا أمر حسن لأنها أعقد نقاط التوسعة. لنستخدم الآن مرشح lipsum:

{{ 40|lipsum }}

وهذا يعمل أيضًا لكن يجب أن يحول المرشحُ القيمةَ الممرَّرة إلى شيء آخر، وهنا نستخدم القيمة للإشارة إلى عدد الكلمات التي يجب توليدها، لذا فإن 40 هي وسيط argument للمرشح، وليست القيمة التي نريد تحويلها. لنستخدم الآن دالة lipsum:

{{ lipsum(40) }}

ولهذا المثال خاصة فإن نقطة التوسعة التي ستُستخدم هي إنشاء الدالة، وتستطيع استخدامها في أي مكان يقبل تعبيرًا:

{{ 'some text' ~ lipsum(40) ~ 'some more text' }}

{% set lipsum = lipsum(40) %}

وأخيرًا تستطيع استخدام كائنًا عامًا global object مع تابع قادر على إنشاء نص لوريم إبسوم:

{{ text.lipsum(40) }}

وكقاعدة عامة، استخدم الدوال في حالة المزايا التي يكثر استخدامها، والكائنات العامة لأي شيء آخر، واعتبر بالجدول التالي عند توسيع Twig:

طرق توسيع Twig وحالات استخدامها
الطريقة صعوبة التنفيذ التواتر متى
شيفرة جامعة macro بسيط متكرر إنشاء المحتوى
عام global بسيط متكرر كائن مساعد
دالة function بسيط متكرر تَحوُّل القيمة
وسم tag معقد نادر بنية خاصة للغة Domain Specific Language
اختبار test بسيط نادر إجراء بولياني
عامل operator بسيط نادر تَحوُّل القيمة

المتغيرات العامة Global Variables

المتغير العام يشبه أي متغير قالب آخر إلا أنه متاح في جميع القوالب والشيفرات الجامعة:

$twig = new \Twig\Environment($loader);
$twig->addGlobal('text', new Text());

تستطيع حينها استخدام متغير text في أي مكان داخل القالب:

{{ text.lipsum(40) }}

المرشحات

إنشاء المرشح يكون بربط اسم مع نوع البيانات "callable" من PHP:

// دالة مجهولة
$filter = new \Twig\TwigFilter('rot13', function ($string) {
    return str_rot13($string);
});

// بسيطة PHP أو دالة
$filter = new \Twig\TwigFilter('rot13', 'str_rot13');

// class static method أو تابع صنف ساكن
$filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);
$filter = new \Twig\TwigFilter('rot13', 'SomeClass::rot13Filter');

// أو تابع صنف
$filter = new \Twig\TwigFilter('rot13', [$this, 'rot13Filter']);
// هذا الأخير يحتاج إلى تطبيق في وقت التشغيل، انظر أدناه للتوضيح.
$filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);

يكون أول وسيط ممرَّر إلى منشئ ‎\Twig\TwigFilter هو اسم المرشح الذي ستستخدمه في القوالب والثاني هو نوع البيانات "callable" المرتبط به. ثم بعد ذلك، نضيف المرشح إلى بيئة Twig:

$twig = new \Twig\Environment($loader);
$twig->addFilter($filter);

انظر الآن كيف تستخدمه في قالب:

{{ 'Twig'|rot13 }}

{# Gjvt سيُخرج #}

يستقبل نوع البيانات "callable" (ردود النداء) الجانب الأيسر من المرشح قبل الأنبوب | كوسيط أول عند استدعاء Twig له، وتمرَّر وسائط إضافية إلى المرشح داخل أقواس (). فمثلًا تُصرَّف الشيفرة التالية:

{{ 'TWIG'|lower }}
{{ now|date('d/m/Y') }}

لتكون كما يلي بعد التصريف:

<?php echo strtolower('TWIG') ?>
<?php echo twig_date_format_filter($now, 'd/m/Y') ?>

يأخذ الصنف ‎\Twig\TwigFilter مصفوفة من الخيارات كآخر وسيط له:

$filter = new \Twig\TwigFilter('rot13', 'str_rot13', $options);

المرشحات الواعية للبيئة

إذا أردت الوصول إلى نسخة البيئة الحالية في مرشحك فاجعل الخيار needs_environment على true، وسيمرر Twig البيئة الحالية كأول وسيط إلى استدعاء المرشح:

$filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $string) {
    // اجلب مجموعة المحارف الحالية للنسخة.
    $charset = $env->getCharset();

    return str_rot13($string);
}, ['needs_environment' => true]);

المرشحات الواعية للسياق

إذا أردت الوصول إلى السياق الحالي في مرشحك فاجعل الخيار needs_context على true، وسيمرر Twig السياق الحالي كأول وسيط إلى استدعاء المرشح أو الوسيط الثاني إذا كان needs_environment على true أيضًا:

$filter = new \Twig\TwigFilter('rot13', function ($context, $string) {
    // ...
}, ['needs_context' => true]);

$filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $context, $string) {
    // ...
}, ['needs_context' => true, 'needs_environment' => true]);

التهريب التلقائي Automatic Escaping

إذا كان التهريب التلقائي مفعلًا فقد يُهرَّب خرج المرشح قبل الطباعة، وإن كان مرشحك يتصرف كمهرِّب أو يخرج شيفرة HTML أو جافاسكربت فربما تريد طباعة الخرج الخام، فاضبط خيار is_safe في مثل تلك الحالة:

$filter = new \Twig\TwigFilter('nl2br', 'nl2br', ['is_safe' => ['html']]);

قد تحتاج بعض المرشحات إلى التعامل مع مدخلات مهرَّبة سلفًا أو آمنة، كما في حالة إضافة وسوم HTML آمنة إلى خرج غير آمن، ففي مثل تلك الحالة، اضبط خيار pre_escape ليهرب بيانات الدخل قبل تشغيلها عبر المرشح الخاص بك:

$filter = new \Twig\TwigFilter('somefilter', 'somefilter', ['pre_escape' => 'html', 'is_safe' => ['html']]);

المرشحات المتغيرة Variadic Filters

إذا وجب على مرشح أن يقبل عددًا عشوائيًا من الوسائط، فاجعل خيار is_variadic على true، كي يمرِّر Twig الوسائط الإضافية كوسيط أخير في هيئة مصفوفة إلى استدعاء المرشح:

$filter = new \Twig\TwigFilter('thumbnail', function ($file, array $options = []) {
    // ...
}, ['is_variadic' => true]);

لكن انتبه إلى أن الوسائط المسمَّاة named arguments التي تمرَّر إلى مرشح متغير لا يمكن التحقق من صلاحيتها بما أنها ستؤول تلقائيًا إلى مصفوفة الخيارات option array.

المرشحات الديناميكية

المرشح الديناميكي هو مرشح يحتوي اسمه على المحرف الخاص *، وهذا المحرف مطابق لأي سلسلة نصية:

$filter = new \Twig\TwigFilter('*_path', function ($name, $arguments) {
    // ...
});

المرشحات التالية مطابقة للمرشح الديناميكي المعرَّف أعلاه:

  • product_path
  • category_path

يستطيع المرشح الديناميكي أن يعرِّف أكثر من جزء ليكون ديناميكيًا أيضًا:

$filter = new \Twig\TwigFilter('*_path_*', function ($name, $suffix, $arguments) {    // ...});

يستقبل المرشح كل قيم الجزء الديناميكي قبل وسائط المرشح العادي، لكن بعد البيئة والسياق، فمثلًا، إذا استدعينا ‎'foo'|a_path_b()‎ فستُمرَّر الوسائط ('a', 'b', 'foo') إلى المرشح.

المرشحات المهملة Deprecated Filters

تستطيع تحديد المرشح ليكون مهملًا من خلال ضبط الخيار deprecated على true، كما تستطيع تحديد مرشح بديل يكون بديلًا للمهمل إذا رأيت ذلك:

$filter = new \Twig\TwigFilter('obsolete', function () {
    // ...
}, ['deprecated' => true, 'alternative' => 'new_one']);

عند إهمال المرشح فإن Twig يرسل إشعار إهمال عند تصريف قالب يستخدم ذلك المرشح، انظر صفحة الوصفات للمزيد.

الدوال

تعرَّف الدوال بنفس طريقة تعريف المرشحات، لكن تحتاج إلى إنشاء نسخة من ‎\Twig\TwigFunction:

$twig = new \Twig\Environment($loader);
$function = new \Twig\TwigFunction('function_name', function () {
    // ...
});
$twig->addFunction($function);

تدعم الدوال نفس المزايا التي تدعمها المرشحات ما عدا خياري pre_escape و preserves_safety.

الاختبارات

تعَّرف الاختبارات بنفس طريقة تعريف الدوال والمرشحات، لكن تحتاج إلى إنشاء نسخة من ‎\Twig\TwigTest:

$twig = new \Twig\Environment($loader);
$test = new \Twig\TwigTest('test_name', function () {
    // ...
});
$twig->addTest($test);

تسمح لك الاختبارات بإنشاء منطق يختص بتطبيق ما من أجل تقييم الشرطيات البوليانية boolean conditions. مثلًا، لتنشئ اختبار Twig يتحقق إن كانت الكائنات حمراء red:

$twig = new \Twig\Environment($loader);
$test = new \Twig\TwigTest('red', function ($value) {
    if (isset($value->color) && $value->color == 'red') {
        return true;
    }
    if (isset($value->paint) && $value->paint == 'red') {
        return true;
    }
    return false;
});
$twig->addTest($test);

يجب أن تعيد دوال الاختبار إما true أو false. استخدم الخيار node_class عند إنشاء الاختبارات من أجل توفير تجميعة مخصصة للاختبار، وذلك ينفعك إن كان يمكن تصريف اختبارك داخل أنوع البيانات الأولية لـ PHP، وتستخدمه اختبارات عديدة مضمنة في Twig:

namespace App;

use Twig\Environment;
use Twig\Node\Expression\TestExpression;
use Twig\TwigTest;

$twig = new Environment($loader);
$test = new TwigTest(
    'odd',
    null,
    ['node_class' => OddTestExpression::class]);
$twig->addTest($test);

class OddTestExpression extends TestExpression
{
    public function compile(\Twig\Compiler $compiler)
    {
        $compiler
            ->raw('(')
            ->subcompile($this->getNode('node'))
            ->raw(' % 2 != 0')
            ->raw(')')
        ;
    }
}

يوضح المثال أعلاه كيف تنشئ اختبارات تستخدم صنف العقدة node class الذي يكون له وصول إلى عقدة فرعية اسمها node، تحتوي على القيمة التي تُختبر.

{% if my_value is odd %}

إذا استُخدم مرشح odd في شيفرة مثل التي أعلاه فإن عقدة node ستحتوي على تعبير لـ my_value، ويكون للاختبارات المبنية على عقد وصول أيضًا إلى عقدة arguments، وستحتوي تلك العقدة على وسائط أخرى عديدة تم توفيرها لاختبارك.

وإذا أردت تمرير عددًا متغيرًا من الوسائط الموضعية positional أو المسماة named إلى الاختبار فاضبط خيار is_variadic على true، إذ أن الاختبارات تدعم الأسماء الديناميكية انظر المرشحات الديناميكية للبنية اللغوية Syntax.

الوسوم

إحدى المزايا المثيرة في محرك قوالب مثل Twig هو إمكانية تعريف بنيات لغوية جديدة، وهي إحدى أعقد الإمكانيات التي تتعرض لها إذا أردت فهم كيفية عمل Twig، لكننا لن نحتاج إلى وسم في أغلب الأحيان:

  • إذا كان وسمك يولد بعض المخرجات فاستخدم دالة.
  • إذا كان وسمك يعدل بعض المحتوى ويعيده فاستخدم مرشحًا.

مثلًا، إذا أردت إنشاء وسم يحول نصًا بصيغة مارك داون إلى HTML، فأنشئ مرشح markdown بدلًا من إنشاء وسم:

{{ '**markdown** text'|markdown }}

إذا أردت استخدام هذا المرشح على نصوص كثيرة فغلفه بوسم apply:

{% apply markdown %}
Title
=====

Much better than creating a tag as you can **compose** filters.
{% endapply %}

إذا لم يكن وسمك يخرج أي شيء لكنه موجود بسبب أثر جانبي فأنشئ دالة لا تعيد شيئًا واستدعها من خلال وسم filter، فمثلًا إذا أردت إنشاء وسم يسجل النصوص فأنشئ دالة log واستدعها من خلال وسم do:

{% do log('Log some things') %}

والآن لننشئ وسمًا لبُنيةٍ لغوية جديدة وليكن set الذي يسمح بتعريف متغيرات بسيطة من داخل القالب، ويمكن استخدام ذلك الوسم كما يلي:

{% set name = "value" %}

{{ name }}

{# should output value #}

لاحظ أن وسم set جزء من توسيع النواة core وعليه سيكون متاحًا دائمًا، والنسخة المضمنة منه أقوى وتدعم إسنادات متعددة افتراضيًا. ستحتاج إلى ثلاثة خطوات من أجل تعريف وسم جديد:

  • تعريف صنف محلل للوحدات token parser class مسؤول عن تحليل شيفرة القالب.
  • تعريف صنف عقدة يكون مسؤولًا عن تحويل الشيفرة المحلَّلة إلى PHP.
  • تسجيل الوسم.

تسجيل وسم جديد

أضف وسمًا من خلال استدعاء التابع addTokenParser على نسخة ‎\Twig\Environment:

$twig = new \Twig\Environment($loader);
$twig->addTokenParser(new Project_Set_TokenParser());

تعريف محلل مفتاح Token Parser

لننظر الآن في الشيفرة الحقيقية لهذا الصنف:

class Project_Set_TokenParser extends \Twig\TokenParser\AbstractTokenParser
{
    public function parse(\Twig\Token $token)
    {
        $parser = $this->parser;
        $stream = $parser->getStream();

        $name = $stream->expect(\Twig\Token::NAME_TYPE)->getValue();
        $stream->expect(\Twig\Token::OPERATOR_TYPE, '=');
        $value = $parser->getExpressionParser()->parseExpression();
        $stream->expect(\Twig\Token::BLOCK_END_TYPE);

        return new Project_Set_Node($name, $value, $token->getLine(), $this->getTag());
    }

    public function getTag()
    {
        return 'set';
    }
}

يجب أن يعيد التابع getTag()‎ الوسم الذي نريد تحليله، وهو set هنا، ويُستدعى التابع parse()‎ كلما وجد وسم set، ويجب أن يعيد نسخة من ‎\Twig\Node\Node تمثل العقدة (ستجد شرح إنشاء استدعاءات Project_Set_Node لاحقًا في الصفحة).

وتُبسَّط عملية التحليل بفضل مجموعة من التوابع التي يمكنك استدعاءها من مجرى مفتاح التشفير token stream أي ‎$this->parser->getStream()‎:

  • getCurrent()‎: تجلب المفتاح token الحالي في المجرى.
  • next()‎: تتحرك إلى المفتاح التالي في المجرى لكن تعيد المفتاح القديم.
  • test($type) أو test($value) أو test($type, $value): تحدد ما إذا كان المفتاح الحالي من نوع أو قيمة بعينها أو كليهما، وقد تكون القيمة مصفوفة من عدة قيم ممكنة.
  • expect($type[, $value[, $message]]): إذا لم يكن المفتاح الحالي من النوع المعطى أو القيمة المعطاة فيُرفع خطأ لغوي syntax error، وإلا إذا كان النوع صحيحًا وكذلك القيمة فيعاد المفتاح وينتقل المجرى إلى الوحدة التالية.
  • look()‎: ينظر إلى الوحدة التالية من غير استهلاكه.
  • يتم تحليل التعابير من خلال استدعاء parseExpression()‎ كما فعلنا في وسم set.

تستطيع تعلم جميع التفاصيل التي في عملية التحليل بقراءة أصناف TokenParser الموجودة فعليًا.

تعريف عقدة

إن الصنف Project_Set_Node نفسه قصير نوعًا ما:

class Project_Set_Node extends \Twig\Node\Node
{
    public function __construct($name, \Twig\Node\Expression\AbstractExpression $value, $line, $tag = null)
    {
        parent::__construct(['value' => $value], ['name' => $name], $line, $tag);
    }

    public function compile(\Twig\Compiler $compiler)
    {
        $compiler
            ->addDebugInfo($this)
            ->write('$context[\''.$this->getAttribute('name').'\'] = ')
            ->subcompile($this->getNode('value'))
            ->raw(";\n")
        ;
    }
}

ينفذ المصرِّف واجهة سلسلة ويوفر توابع تساعد المطور على توليد شيفرات PHP جميلة وسهلة القراءة:

  • subcompile()‎: يصرِّف العقدة.
  • raw()‎: يكتب السلسلة النصية كما هي.
  • write()‎: يكتب السلسلة النصية المعطاة بإضافة إزاحة في بداية كل سطر.
  • string()‎: يكتب سلسلة نصية بين علامتي اقتباس.
  • repr()‎: يكتب تمثيل PHP لقيمة معطاة، انظر ‎\Twig\Node\ForNode لمثال على استخدامه.
  • addDebugInfo()‎: يضيف السطر المتعلق بالعقدة الحالية من ملف القالب الأصلي كتعليق.
  • indent()‎: يزيح الشيفرة المولَّدة، انظر ‎\Twig\Node\BlockNode لمثال على الاستخدام.
  • outdent()‎: يزيح الشيفرة المولَّدة خارجيًا outdent ، انظر ‎\Twig\Node\BlockNode لمثال على الاستخدام.

إنشاء توسعة Extension

إن ما يجعلك تنشئ توسعة هو نقل الشيفرة المستخدمة بكثرة إلى صنف قابل لإعادة استخدامه مثل إضافة دعم للدُوَلية مثلًا internationalization، بأن يكون التطبيق مستخدمًا في عدة دول بعدة لغات مختلفة. ويستطيع التوسيع أن يعرِّف الوسوم والمرشحات والاختبارات والعوامل والدوال وزوار العقد node visitors .

ومن المفيد غالبًا أن تنشئ توسعة واحدة لمشروعك لاستضافة جميع الوسوم المحددة والمرشحات التي تريد إضافتها إلى Twig. كذلك، عند تحزيم شيفرتك في توسيع فإن Twig من الذكاء بحيث يعيد تصريف قوالبك كلما أجريت تغييرًا فيه، إذا كان auto_reload مفعلًا.

التوسعة Extension هي صنف يطبق الواجهة التالية:

interface \Twig\Extension\ExtensionInterface
{
    /**
     * يعيد نسخ محلل الرموز لإضافته إلى القائمة الحالية
     *
     * @return \Twig\TokenParser\TokenParserInterface[]
     */
    public function getTokenParsers();

    /**
     * يعيد نسخ زائد العقدة لإضافتها إلى القائمة الحالية
     *
     * @return \Twig\NodeVisitor\NodeVisitorInterface[]
     */
    public function getNodeVisitors();

    /**
     * يعيد قائمة من الفلاتر لإضافتها إلى القائمة الحالية
     *
     * @return \Twig\TwigFilter[]
     */
    public function getFilters();

    /**
     * يعيد قائمة من الاختبارات لإضافتها إلى القائمة الحالية
     *
     * @return \Twig\TwigTest[]
     */
    public function getTests();

    /**
     * يعيد قائمة من الدوال لإضافتها إلى القائمة الحالية
     *
     * @return \Twig\TwigFunction[]
     */
    public function getFunctions();

    /**
     * يعيد قائمة من العوامل لإضافتها إلى القائمة الحالية.
     * Returns a list of operators to add to the existing list.
     *
     * @return array<array> First array of unary operators, second array of binary operators
     */
    public function getOperators();
}

إذا أردت الحفاظ على صنف التوسيع الخاص بك رشيقًا فاجعله يرث من الصنف المضمن ‎\Twig\Extension\AbstractExtension بدلًا من تطبيق الواجهة بما أنه يوفر تطبيقات فارغة لجميع التوابع:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
}

لا تفعل هذه التوسعة أي شيء إلى الآن، وسنخصصها فيما يلي من الشرح، وتستطيع حفظها في أي مكان في نظام الملفات بما أن جميع التوسعات يجب أن تُسجل صراحة لتكون متاحة في قوالبك، كما تستطيع تسجيل توسيع باستخدام التابع addExtension()‎ على الكائن الرئيسي Enivronment الخاص بك:

$twig = new \Twig\Environment($loader);
$twig->addExtension(new Project_Twig_Extension());

تستطيع النظر في توسعات النواة core الخاصة بـ Twig إذ هي أمثلة ممتازة على كيفية عمل التوسعات.

المتغيرات العامة

يمكن تسجيل المتغيرات العامة في توسعة من خلال التابع getGlobals()‎:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface
{
    public function getGlobals(): array
    {
        return [
            'text' => new Text(),
        ];
    }

    // ...
}

الدوال

يمكن تسجيل الدوال في توسعة من خلال التابع getFunctions()‎ :

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getFunctions()
    {
        return [
            new \Twig\TwigFunction('lipsum', 'generate_lipsum'),
        ];
    }

    // ...
}

المرشحات

إذا أردت إضافة مرشح إلى توسعة فستحتاج إلى إعادة تعريف التابع getFilters()‎، فيجب أن يعيد هذا التابع مصفوفة من المرشحات لإضافتها إلى بيئة Twig:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getFilters()
    {
        return [
            new \Twig\TwigFilter('rot13', 'str_rot13'),
        ];
    }

    // ...
}

الوسوم

يمكن إضافة الوسوم إلى توسعة بإعادة تعريف override التابع getTokenParsers()‎، فيجب أن يعيد مصفوفةً من الوسوم لإضافتها إلى بيئة Twig:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getTokenParsers()
    {
        return [new Project_Set_TokenParser()];
    }

    // ...
}

أضفنا في الشيفرة السابقة وسمًا واحدًا جديدًا، عرَّفه الصنف Project_Set_TokenParser، وهذا الصنف مسؤول عن تحليل الوسم وتصريفه إلى PHP.

العوامل Operators

يسمح لك التابع getOperators()‎ بإضافة عوامل جديدة، انظر الشيفرة التالية كمثال على كيفية إضافة العوامل ! و || و &&:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getOperators()
    {
        return [
            [
                '!' => ['precedence' => 50, 'class' => \Twig\Node\Expression\Unary\NotUnary::class],
            ],
            [
                '||' => ['precedence' => 10, 'class' => \Twig\Node\Expression\Binary\OrBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
                '&&' => ['precedence' => 15, 'class' => \Twig\Node\Expression\Binary\AndBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
            ],
        ];
    }

    // ...
}

الاختبارات

يسمح لك التابع getTests()‎ بإضافة دوال اختبار جديدة:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getTests()
    {
        return [
            new \Twig\TwigTest('even', 'twig_test_even'),
        ];
    }

    // ...
}

موازنة بين التعريف ووقت التشغيل

يمكن تعريف تنفيذات وقت التشغيل runtime implementations لمرشحات Twig ودواله واختباراته كأي نوع بيانات "callable" في PHP:

  • الدوال/التوابع الساكنة: بسيطة في تطبيقها، وسريعة -تستخدمها جميع توسعات core في Twig، لكن من الصعب أن يعتمد وقت التشغيل runtime على كائنات خارجية.
  • الإغلاقات closures : بسيطة في تنفيذها.
  • توابع الكائن: أكثر مرونة وتحتاجها إن كانت شيفرة وقت التشغيل لديك تعتمد على كائنات خارجية.

إن أبسط طريقة لاستخدام التوابع هي تعريفها على توسعة نفسها:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    private $rot13Provider;

    public function __construct($rot13Provider)
    {
        $this->rot13Provider = $rot13Provider;
    }

    public function getFunctions()
    {
        return [
            new \Twig\TwigFunction('rot13', [$this, 'rot13']),
        ];
    }

    public function rot13($value)
    {
        return $this->rot13Provider->rot13($value);
    }
}

هذه الطريقة على بساطتها إلا أنه لا يُنصح بها لأنها تجعل تصريف القالب يعتمد على اعتماديات وقت التشغيل حتى لو لم تكن ثمة حاجة إليها. تستطيع فصل تعريفات توسعة من تنفيذات وقت تشغيلها بتسجيل نسخة من ‎\Twig\RuntimeLoader\RuntimeLoaderInterface على البيئة التي تعرف كيفية بدء أصناف وقت التشغيل تلك، ويجب أن تكون أصناف وقت التشغيل قابلة للتحميل التلقائي:

class RuntimeLoader implements \Twig\RuntimeLoader\RuntimeLoaderInterface
{
    public function load($class)
    {
        // $class طبق المنطق لإنشاء نسخة من 
        // وأدخل اعتمادياتها.
        // وسيعني ذلك في الغالب أن تستخدم حاوية حقن الاعتماديات
        // dependency injection container أو 
        if ('Project_Twig_RuntimeExtension' === $class) {
            return new $class(new Rot13Provider());
        } else {
            // ...
        }
    }
}

$twig->addRuntimeLoader(new RuntimeLoader());

لاحظ أن Twig يأتي بمحمِّل وقت تشغيل ‎\Twig\RuntimeLoader\ContainerRuntimeLoader متوافق مع PSR-11، وصار من الممكن أن تنقل منطق وقت التشغيل إلى صنف Project_Twig_RuntimeExtension جديد وتستخدمه مباشرة في التوسيع:

class Project_Twig_RuntimeExtension
{
    private $rot13Provider;

    public function __construct($rot13Provider)
    {
        $this->rot13Provider = $rot13Provider;
    }

    public function rot13($value)
    {
        return $this->rot13Provider->rot13($value);
    }
}

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getFunctions()
    {
        return [
            new \Twig\TwigFunction('rot13', ['Project_Twig_RuntimeExtension', 'rot13']),
            // أو
            new \Twig\TwigFunction('rot13', 'Project_Twig_RuntimeExtension::rot13'),
        ];
    }
}

اختبار توسعة

الاختبارات الدالية

تستطيع إنشاء اختبارات دالّية Functional Tests للتوسعات من خلال إنشاء بنية الملف التالية في مجلد اختبارك:

Fixtures/
    filters/
        foo.test
        bar.test
    functions/
        foo.test
        bar.test
    tags/
        foo.test
        bar.test
IntegrationTest.php

ويجب أن يبدو ملف IntegrationTest.php كما يلي:

use Twig\Test\IntegrationTestCase;

class Project_Tests_IntegrationTest extends IntegrationTestCase
{
    public function getExtensions()
    {
        return [
            new Project_Twig_Extension1(),
            new Project_Twig_Extension2(),
        ];
    }

    public function getFixturesDir()
    {
        return __DIR__.'/Fixtures/';
    }
}

تستطيع العثور على أمثلة لواجهات الاختبار Fixtures داخل مستودع Twig، داخل مجلد tests/Twig/Fixtures.

اختبارات العقد

قد يكون اختبار زوار العقد معقدًا، لذلك اجلب حالات اختبارك عبر الوراثة من ‎\Twig\Test\NodeTestCase، وستجد أمثلة في مستودع Twig، داخل مجلد tests/Twig/Node.

انظر أيضًا

المصادر