الوصفات في محرك القوالب 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 المهملة مثلًا-، فسجِّل معالج خطأ مخصص لالتقاطها مثل المعالج أدناه:
1
2
3
4
5
6
7
8
9
10
$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) ويتخطى (overrride) بعض الكتل.
- الاستبدال: إذا كنت تستخدم محمِّل نظام ملفات فإن 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);
انظر المثال التالي الذي يوضح بعض الإعدادات التي تحاكي البنية اللغوية لمحركات قوالب أخرى:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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()
كما هو موضح في الشيفرة التالية:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 يستخدمه لزيارة جميع القوالب التي يصرِّفها، فإذا أردت الإبقاء على بعض بيانات الحالة فربما تود إعادة ضبطها عند زيارة قالب جديد، ويمكن فعل هذا بالشيفرة التالية:
1
2
3
4
5
6
7
8
9
10
11
12
13
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 مؤقتة داخل الذاكرة للعمل معها:
1
2
3
4
5
6
7
8
9
10
$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
. لنعرِّف الآن محملًا قادرًا على استخدام قاعدة البيانات تلك:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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' => ['{[', ']}'], ]));
- إذا كنت ستدخِل AngularJS على Twig فغير إقحام الوسوم باستخدام خدمة
انظر أيضًا
- المكونات الداخلية لمحرك القوالب Twig.
- Twig لمصممي القوالب
- معايير كتابة الشيفرات في محرك القوالب Twig.