وصفات خاصة لمحرك القوالب Twig

من موسوعة حسوب

تعرض هذه الصفحة وصفات سرية تستحق الإطلاع عليها قد تساعدك في عملك مع محرك قوالب Twig ووضبطه وتخصيص استخدامك المتقدم له.

عرض إشعارات الإهمال Deprecation Notices

تولِّد المزايا المهملة إشعارات إهمال من خلال استدعاء إلى الدالة trigger_error()‎ في لغة PHP، وتُسكَت تلك الإشعارات افتراضيًا ولا تُعرض أو تسجَّل أبدًا، فإذا أردت إزالة جميع استخدامات المزايا المهملة من قوالبك فاكتب سكربت وشغلها مع الأسطر التالية:

require_once __DIR__.'/vendor/autoload.php';

$twig = create_your_twig_env();

$deprecations = new \Twig\Util\DeprecationCollector($twig);

print_r($deprecations->collectDir(__DIR__.'/templates'));

يصرِّف التابع collectDir()‎ جميع القوالب الموجودة في المجلد ويلتقط إشعارات الإهمال ويعيدها، فإذا لم تكن قوالبك مخزَّنة في نظام الملفات فاستخدم التابع collect()‎ الذي يأخذ Traversable الذي يجب أن يعيد بدوره أسماء قوالب كمفاتيح، ومحتويات قوالب كقيَم، كما في ‎\Twig\Util\TemplateDirIterator، لكن لن تعثر هذه الشيفرة على كل الإهمالات -كأصناف Twig المهملة مثلًا-، فسجِّل معالج خطأ مخصص لالتقاطها مثل المعالج أدناه:

$deprecations = [];
set_error_handler(function ($type, $msg) use (&$deprecations) {
    if (E_USER_DEPRECATED === $type) {
        $deprecations[] = $msg;
    }
});

// شغِّل تطبيقك

print_r($deprecations);

لاحظ أن أغلب إشعارات الإهمال تُطلَق أثناء التصريف compilation، لذا لن تولَّد إذا كانت القوالب مخزَّنة أصلًا، وإذا أردت إدارة إشعارات الإهمال من اختبارات PHPUnit الخاصة بك فانظر حزمة symfony/phpunit-bridge إذ ستيسر عليك تلك المهمة.

جعل التخطيطات شرطية

إن العمل مع Ajax يعني أن بعض المحتوى قد يُعرض أحيانًا كما هو، وقد يزخرَف أحيانًا أخرى بتخطيط أو layout، وبما إن أسماء قوالب تخطيطات Twig قد تكون أي تعبير صالح فيمكنك تمرير متغير يقيَّم إلى true عند إنشاء الطلب من خلال Ajax، ويختار التخطيط وفقًا لذلك:

{% extends request.ajax ? "base_ajax.html" : "base.html" %}

{% block content %}
    This is the content to be displayed.
{% endblock %}

جعل الإدراج ديناميكي

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

{% include var ~ '_foo.html' %}

إذا كانت var تقيَّم إلى index فسيتم تصيير render القالب index_foo.html، وعمومًا قد يكون اسم القالب أي تعبير صالح، انظر:

{% include var|default('index') ~ '_foo.html' %}

استبدال قالب يرث نفسه

يمكن تخصيص القالب بإحدى طريقتين:

  • الوراثة inheritance: يرث القالب من قالب أب parent template ويبدل override بعض الكتل الموروثة ويعدلها.
  • الاستبدال Replacement: إذا كنت تستخدم محمِّل نظام ملفات فإن Twig يحمِّل أول قالب يجده في قائمة المجلدات المهيأة، ويحل القالب الموجود في المجلد مكان قالب آخر من مجلد أبعد في القائمة.

لكن كيف نستبدل قالبًا يرث نفسه -قالب في مجلد أبعد في القائمة-؟

لنقل أنك قوالبك حُمِّلت من كل من ‎.../templates/mysite و ‎.../templates/default على الترتيب، فعندئذ يكون القالب page.twig المخزَّن في ‎.../templates/default كما يلي:

{# page.twig #}
{% extends "layout.twig" %}

{% block content %}
{% endblock %}

تستطيع استبدال هذا القالب من خلال وضع ملف بنفس الاسم في ‎.../templates/mysite، وإذا أردت وراثة القالب الأصلي فربما تكتب شيئًا كما يلي:

{# page.twig in .../templates/mysite #}
{% extends "page.twig" %} {# from .../templates/default #}

غير أن هذا لن يعمل بما أن Twig يحمّل القالب من ‎.../templates/mysite، لكن تبين أننا نستطيع إصلاح ذلك بإضافة مجلد في نهاية مجلدات القوالب لديك، وسيكون أبًا (مجلدًا رئيسيًا) لجميع المجلدات الأخرى وهو ‎.../templates في حالتنا نحن. وهذا يجعل كل ملف قالب داخل نظامنا له عنوان فريد. ورغم أنك ستستخدم المسارات العادية أغلب الوقت، لكن ستكون ثمة حالات خاصة تريد فيها وراثة قالب عبر تخطي نسخة منه فتستطيع الإشارة إلى مسار القالب الكامل للمجلد الأب له في وسم extends:

{# page.twig in .../templates/mysite #}
{% extends "default/page.twig" %} {# from .../templates #}

لاحظ أن هذه الوصفة مستقاة من هذه الصفحة من ويكي Django.

تخصيص البنية اللغوية

يسمح Twig ببعض التخصيصات في البنية اللغوية syntax لمحدِّدات الكتل، ولا يُنصح باستخدام هذه الميزة بما أن القوالب ستكون مرتبطة ببنيتك المخصصة، لكن قد يكون ذلك هو الخيار الأنسب في بعض المشاريع الخاصة. ستحتاج إلى إنشاء كائن معجم lexer خاص بك لتغيير محددات الكتل :

$twig = new \Twig\Environment(...);

$lexer = new \Twig\Lexer($twig, [
    'tag_comment'   => ['{#', '#}'],
    'tag_block'     => ['{%', '%}'],
    'tag_variable'  => ['{{', '}}'],
    'interpolation' => ['#{', '}'],
]);
$twig->setLexer($lexer);

انظر المثال التالي الذي يوضح بعض الإعدادات التي تحاكي البنية اللغوية لمحركات قوالب أخرى:

// Ruby erb syntax
$lexer = new \Twig\Lexer($twig, [
    'tag_comment'  => ['<%#', '%>'],
    'tag_block'    => ['<%', '%>'],
    'tag_variable' => ['<%=', '%>'],
]);

// SGML Comment Syntax
$lexer = new \Twig\Lexer($twig, [
    'tag_comment'  => ['<!--#', '-->'],
    'tag_block'    => ['<!--', '-->'],
    'tag_variable' => ['${', '}'],
]);

// Smarty like
$lexer = new \Twig\Lexer($twig, [
    'tag_comment'  => ['{*', '*}'],
    'tag_block'    => ['{', '}'],
    'tag_variable' => ['{$', '}'],
]);

استخدام الخصائص الديناميكية للكائن

يحاول Twig حين يقابل متغيرًا مثل article.title أن يجد خاصية title عامة في كائن article، وهذا يصلح أيضًا إذا كانت الخاصية غير موجودة لكنها معرَّفة ديناميكيًا بفضل التابع ‎_get()‎، فتحتاج حينئذ أن تستخدم التابع ‎_isset()‎ كما هو موضح في الشيفرة التالية:

class Article
{
    public function __get($name)
    {
        if ('title' == $name) {
            return 'The title';
        }

        // ارفع خطأً ما هنا
    }

    public function __isset($name)
    {
        if ('title' == $name) {
            return true;
        }

        return false;
    }
}

الوصول إلى السياق الأب في الحلقات التكرارية المتشعبة

قد تحتاج أحيانًا عند استخدام الحلقات التكرارية المتشعبة nested loops إلى أن تصل إلى السياق الأب، ويتم ذلك عن طريق المتغير loop.parent، فمثلًا إذا كانت لديك بيانات القالب التالية:

$data = [
    'topics' => [
        'topic1' => ['Message 1 of topic 1', 'Message 2 of topic 1'],
        'topic2' => ['Message 1 of topic 2', 'Message 2 of topic 2'],
    ],
];

والقالب التالي من أجل عرض جميع الرسائل في كل الموضوعات:

{% for topic, messages in topics %}
    * {{ loop.index }}: {{ topic }}
  {% for message in messages %}
      - {{ loop.parent.loop.index }}.{{ loop.index }}: {{ message }}
  {% endfor %}
{% endfor %}

فإن الخرج سيكون شبيهًا بما يلي:

* 1: topic1
  - 1.1: The message 1 of topic 1
  - 1.2: The message 2 of topic 1
* 2: topic2
  - 2.1: The message 1 of topic 2
  - 2.2: The message 2 of topic 2

يُستخدم المتغير loop.parent في الحلقة الداخلية للوصول إلى السياق الخارجي، لذا يمكن الوصول إلى فهرس الموضوع topic الحالي المعرَّف في حلقة for الخارجية من خلال المتغير loop.parent.loop.index.

التعريف السريع للدوال غير المعرفة والمرشحات والوسوم

أضيف التابع registerUndefinedTokenParserCallback()‎ في الإصدار 3.2 من Twig.

إذا كانت الدالة غير معرَّفة -أو الوسم أو المرشح- فإن Twig يرفع اعتراض‎\Twig\Error\SyntaxError افتراضيًا، لكن قد يستدعي استدعاءً خلفيًا callback قد يكون أي نوع بيانات "callable" صالح، والذي يعيد وسمًا أو دالة أو مرشحًا (فلتر). وبالنسبة للوسوم، سجِّل الاستدعاءات الخلفية باستخدام registerUndefinedTokenParserCallback()‎، أما المرشحات فسجلها باستخدام registerUndefinedFilterCallback()‎، وبالمثل بالنسبة للدوال، سجلها باستخدام registerUndefinedFunctionCallback()‎:

// Twig المضمنة لتكون دوال PHP سجل جميع دوال
// هذا غير آمن، ولا تنفذه في مشروع أبدًا
$twig->registerUndefinedFunctionCallback(function ($name) {
    if (function_exists($name)) {
        return new \Twig\TwigFunction($name, $name);
    }

    return false;
});

إذا لم يستطع نوع البيانات "callable" أن يعيد وسمًا صالحًا أو دالة أو مرشحًا صالحيْن فيجب أن يعيد false، وإذا سجلت أكثر من رد نداء callable واحد فسيستدعيها Twig إلى أن يعيد واحد منها شيئًا غير false. وبما أن حل resolution الدوال والمرشحات والوسوم يتم أثناء التصريف فليس ثمة حمل زائد عند تسجيل هذه الاستدعاءات الخلفية.

التحقق من البنية اللغوية للقالب

قد ترغب في التحقق من البنية اللغوية للقالب إذا جاءت شيفرته من طرف ثالث -من واجهة ويب مثلًا-. انظر كيف تتحقق منها إذا كانت شيفرة القالب مخزنة في متغير ‎$template:

try {
    $twig->parse($twig->tokenize(new \Twig\Source($template)));

    // صالح $template المتغير 
} catch (\Twig\Error\SyntaxError $e) {
    // على خطأ لغوي أو أكثر $template يحتوي المتغير 
}

إذا نفذت تكرارًا على مجموعة من الملفات فتستطيع تمرير اسم الملف إلى التابع tokenize()‎ للحصول على اسم الملف في رسالة الاعتراض:

foreach ($files as $file) {
    try {
        $twig->parse($twig->tokenize(new \Twig\Source($template, $file->getFilename(), $file)));

        // صالح $template المتغير 
    } catch (\Twig\Error\SyntaxError $e) {
        // على خطأ لغوي أو أكثر $template يحتوي المتغير 
    }
}

لاحظ أن هذا التابع لن يلتقط أي خروقات في سياسة صندوق الاختبار sandbox لأن تلك السياسة تنفَّذ أثناء تصيير القالب بما أن Twig يحتاج إلى السياق لإجراء بعض الفحوصات مثل التوابع المسموح بها في الكائنات.

تحديث القوالب المعدلة عند تفعيل OPcache أو APC

إذا كنت تستخدم OPcache وكان opcache.validate_timestamps مضبوطًا على 0 أو كنت تستخدم APC وكان apc.stat مضبوطًا على 0، فلن يتم تحديث الذاكرة المؤقتة للقالب بمجرد إفراغها. ولحل ذلك، أجبر Twig على إبطال ذاكرة bytecode المؤقتة:

$twig = new \Twig\Environment($loader, [
    'cache' => new \Twig\Cache\FilesystemCache('/some/cache/path', \Twig\Cache\FilesystemCache::FORCE_BYTECODE_INVALIDATION),
    // ...
]);

إعادة استخدام زائر عقدة كبير

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

protected $someTemplateState = [];

public function enterNode(\Twig\Node\Node $node, \Twig\Environment $env)
{
    if ($node instanceof \Twig\Node\ModuleNode) {
        // إعادة ضبط الحالة عند الدخول إلى قالب جديد.
        $this->someTemplateState = [];
    }

    // ...

    return $node;
}

استخدام قاعدة بيانات لتخزين القوالب

إذا كنت تطور نظامًا لإدارة محتوى Content Management System، فإن القوالب تُخزن حينها في قاعدة بيانات عادة، وتعطيك الوصفة التالية محملًا بسيطًا لقالب PDO تستطيع استخدامه كنقطة انطلاق تبني عليها. لننشئ أولًا قاعدة بيانات SQLite3 مؤقتة داخل الذاكرة للعمل معها:

$dbh = new PDO('sqlite::memory:');
$dbh->exec('CREATE TABLE templates (name STRING, source STRING, last_modified INTEGER)');
$base = '{% block content %}{% endblock %}';
$index = '
{% extends "base.twig" %}
{% block content %}Hello {{ name }}{% endblock %}
';
$now = time();
$dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['base.twig', $base, $now]);
$dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['index.twig', $index, $now]);

إن ما أنشأناه كان جدول templates بسيط يستضيف قالبين هما base.twig و index.twig. لنعرِّف الآن محملًا قادرًا على استخدام قاعدة البيانات تلك:

class DatabaseTwigLoader implements \Twig\Loader\LoaderInterface
{
    protected $dbh;

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

    public function getSourceContext(string $name): Source
    {
        if (false === $source = $this->getValue('source', $name)) {
            throw new \Twig\Error\LoaderError(sprintf('Template "%s" does not exist.', $name));
        }

        return new \Twig\Source($source, $name);
    }

    public function exists(string $name)
    {
        return $name === $this->getValue('name', $name);
    }

    public function getCacheKey(string $name): string
    {
        return $name;
    }

    public function isFresh(string $name, int $time): bool
    {
        if (false === $lastModified = $this->getValue('last_modified', $name)) {
            return false;
        }

        return $lastModified <= $time;
    }

    protected function getValue($column, $name)
    {
        $sth = $this->dbh->prepare('SELECT '.$column.' FROM templates WHERE name = :name');
        $sth->execute([':name' => (string) $name]);

        return $sth->fetchColumn();
    }
}

وفيما يلي مثال على كيفية استخدامه:

$loader = new DatabaseTwigLoader($dbh);
$twig = new \Twig\Environment($loader);

echo $twig->render('index.twig', ['name' => 'Fabien']);

استخدام مصادر مختلفة للقوالب

الوصفة التالية تمامٌ على ما قبلها، فحتى لو كنت خزنت القوالب التي كتبتها في قاعدة بيانات فستحتاج إلى تخزين القوالب الأساسية أو الأصلية في نظام الملفات، وستحتاج إلى استخدام المحمِّل ‎\Twig\Loader\ChainLoader إذا أمكن تحميل القوالب من مصادر مختلفة.

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

$loader1 = new DatabaseTwigLoader($dbh);
$loader2 = new \Twig\Loader\ArrayLoader([
    'base.twig' => '{% block content %}{% endblock %}',
]);
$loader = new \Twig\Loader\ChainLoader([$loader1, $loader2]);

$twig = new \Twig\Environment($loader);

echo $twig->render('index.twig', ['name' => 'Fabien']);

تعرَّف قوالب base.twig الآن في محمل مصفوفة تستطيع حذفها من قاعدة البيانات دون تعطيل أي شيء.

تحميل قالب من سلسلة نصية

تستطيع تحميل قالب مخزن في سلسلة نصية من قالب آخر عن طريق الدالة template_from_string، من التوسعة ‎\Twig\Extension\StringLoaderExtension:

{{ include(template_from_string("Hello {{ name }}")) }}

وتستطيع تحميل القالب المخزن في سلسلة نصية من PHP عن طريق ‎\Twig\Environment::createTemplate()‎:

$template = $twig->createTemplate('hello {{ name }}');
echo $template->render(['name' => 'Fabien']);

استخدام Twig و AngularJS في نفس القالب

لا يُحبَّذ خلط بنى لغوية لقوالب مختلفة في نفس الملف بما أن Twig و AngularJS يستخدمان نفس المحدِّدات {{ و }} في بنياتهما. لكن إذا أردت استخدام Twig و AngularJS في نفس القالب فثم طريقتين تختار إحداهما وفقًا لمقدار ما تريد إدخاله من AngularJS في قوالبك:

  • تهريب محددات AngularJS بتغليف أقسامها بوسم {% verbatim %} أو بتهريب كل محدد باستخدام {{ '{{' }} و {{ '}}' }}.
  • تغيير محددات أحد محركي القوالب وفقًا للمحرك الذي ستدخله آخرًا:
    • إذا كنت ستدخِل AngularJS على Twig فغيِّر إقحام الوسوم باستخدام خدمة interpolateProvider، كما في حالة وقت تهيئة الوحدات module initialization time:
angular.module('myApp', []).config(function($interpolateProvider) {
  $interpolateProvider.startSymbol('{[').endSymbol(']}');
});
    • أما إذا كان Twig هو الثاني فغير المحدِّدات باستخدام الخيار المعجمي tag_variable:
$env->setLexer(new \Twig\Lexer($env, [
    'tag_variable' => ['{[', ']}'],
]));

انظر أيضًا

المصادر