食谱¶
显示弃用通知¶
不推荐的功能生成弃用通知(通过调用 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()
方法。 collect()
采取了 Traversable
它必须将模板名称作为键返回,将模板内容作为值返回(如 \Twig\Util\TemplateDirIterator
)
但是,这段代码不会找到所有的不推荐使用的代码(比如使用一些不推荐使用的Twig类)。要捕获所有通知,请注册一个自定义错误处理程序,如下所示:
$deprecations = [];
set_error_handler(function ($type, $msg) use (&$deprecations) {
if (E_USER_DEPRECATED === $type) {
$deprecations[] = $msg;
}
});
// run your application
print_r($deprecations);
请注意,大多数弃用通知都是在 汇编 ,因此当模板已经缓存时,不会生成它们。
小技巧
如果要管理PHPUnit测试中的弃用通知,请查看 symfony/phpunit-bridge 包,简化了过程。
设置布局条件¶
使用Ajax意味着相同的内容有时按原样显示,有时用布局装饰。由于Twig布局模板名称可以是任何有效表达式,因此可以传递一个计算结果为的变量 true
当通过Ajax发出请求并相应地选择布局时:
1 2 3 4 5 | {% extends request.ajax ? "base_ajax.html" : "base.html" %}
{% block content %}
This is the content to be displayed.
{% endblock %}
|
使Include动态¶
包含模板时,其名称不必是字符串。例如,名称可以依赖于变量的值:
1 | {% include var ~ '_foo.html' %}
|
如果 var
评估为 index
, the index_foo.html
将呈现模板。
实际上,模板名称可以是任何有效的表达式,例如:
1 | {% include var|default('index') ~ '_foo.html' %}
|
重写同时扩展自身的模板¶
可以通过两种不同的方式自定义模板:
- 遗传 :模板 延伸 父模板和覆盖一些块;
- 替换 :如果使用文件系统加载器,Twig将加载它在已配置目录列表中找到的第一个模板;在目录中找到的模板 替换 另一个来自列表中的目录。
但如何将两者结合起来: 代替 一个同时扩展自身的模板(也称为列表中某个目录中的模板)?
假设您的模板是从这两个加载的 .../templates/mysite
和 .../templates/default
按这个顺序。这个 page.twig
模板,存储在 .../templates/default
内容如下:
1 2 3 4 5 | {# page.twig #}
{% extends "layout.twig" %}
{% block content %}
{% endblock %}
|
您可以通过将具有相同名称的文件放入 .../templates/mysite
. 如果您想扩展原始模板,您可能会尝试编写以下内容:
1 2 | {# page.twig in .../templates/mysite #}
{% extends "page.twig" %} {# from .../templates/default #}
|
但是,这将不起作用,因为Twig总是从中加载模板 .../templates/mysite
.
事实证明,通过在模板目录末尾添加一个目录(它是所有其他目录的父目录)来实现这一点是有可能的: .../templates
在我们的案子里。这将使系统中的每个模板文件都具有唯一的可寻址性。大多数时候您都会使用“正常”路径,但是在特殊情况下,如果希望扩展模板,且模板本身具有覆盖版本,我们可以在扩展标记中引用其父级的完整、明确的模板路径:
1 2 | {# page.twig in .../templates/mysite #}
{% extends "default/page.twig" %} {# from .../templates #}
|
注解
这个食谱的灵感来自以下Django wiki页面:https://code.djangproject.com/wiki/ExtendingTemplates
自定义语法¶
Twig允许对块分隔符进行一些语法自定义。它是 not 建议使用此功能,因为模板将与自定义语法绑定。但是对于特定的项目,更改默认值是有意义的。
要更改块分隔符,您需要创建自己的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' => ['{$', '}'],
]);
使用动态对象特性¶
当嫩枝遇到一个变量 article.title
,它试图找到 title
公共财产 article
对象。
如果属性不存在,但由于魔法的作用是动态定义的,那么它也可以工作 __get()
方法;还需要实现 __isset()
如以下代码片段所示的魔术方法:
class Article
{
public function __get($name)
{
if ('title' == $name) {
return 'The title';
}
// throw some kind of error
}
public function __isset($name)
{
if ('title' == $name) {
return true;
}
return false;
}
}
访问嵌套循环中的父上下文¶
有时,使用嵌套循环时,需要访问父上下文。父上下文始终可以通过 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'],
],
];
以及以下模板以显示所有主题中的所有消息:
1 2 3 4 5 6 | {% for topic, messages in topics %}
* {{ loop.index }}: {{ topic }}
{% for message in messages %}
- {{ loop.parent.loop.index }}.{{ loop.index }}: {{ message }}
{% endfor %}
{% endfor %}
|
输出将类似于:
1 2 3 4 5 6 | * 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
变量。
在飞翔上定义未定义的函数、过滤器和标签¶
3.2 新版功能: 这个 registerUndefinedTokenParserCallback()
方法是在Twig3.2中添加的。
当未定义函数/过滤/标记时,Twig默认为抛出 \Twig\Error\SyntaxError
例外情况。但是,它也可以调用 callback (任何有效的可调用PHP),它应该返回一个函数/过滤/标记。
对于标签,使用注册回调 registerUndefinedTokenParserCallback()
。对于筛选器,使用注册回调 registerUndefinedFilterCallback()
。对于函数,请使用 registerUndefinedFunctionCallback()
::
// auto-register all native PHP functions as Twig functions
// NEVER do this in a project as it's NOT secure
$twig->registerUndefinedFunctionCallback(function ($name) {
if (function_exists($name)) {
return new \Twig\TwigFunction($name, $name);
}
return false;
});
如果可调用对象无法返回有效的函数/过滤/标记,则必须返回 false
。
如果您注册了多个回调,Twig将依次调用它们,直到其中一个不返回为止 false
.
小技巧
由于函数/过滤器/标记的解析是在编译期间完成的,因此在注册这些回调时不会产生任何开销。
正在验证模板语法¶
当模板代码由第三方提供时(例如通过web界面),在保存模板之前验证模板语法可能会很有趣。如果模板代码存储在 $template
变量,这里是你可以做到的:
try {
$twig->parse($twig->tokenize(new \Twig\Source($template)));
// the $template is valid
} catch (\Twig\Error\SyntaxError $e) {
// $template contains one or more syntax errors
}
如果迭代一组文件,可以将文件名传递给 tokenize()
方法获取异常消息中的文件名:
foreach ($files as $file) {
try {
$twig->parse($twig->tokenize(new \Twig\Source($template, $file->getFilename(), $file)));
// the $template is valid
} catch (\Twig\Error\SyntaxError $e) {
// $template contains one or more syntax errors
}
}
注解
此方法不会捕获任何沙盒策略冲突,因为该策略是在模板呈现期间强制执行的(因为Twig需要上下文来进行某些检查,比如对象上允许的方法)。
启用OPcache或APC时刷新修改的模板¶
与一起使用OPcache时 opcache.validate_timestamps
设置为 0
或APC apc.stat
设置为 0
启用Twig缓存,清除模板缓存不会更新缓存。
要解决这个问题,请强制Twig使字节码缓存无效:
$twig = new \Twig\Environment($loader, [
'cache' => new \Twig\Cache\FilesystemCache('/some/cache/path', \Twig\Cache\FilesystemCache::FORCE_BYTECODE_INVALIDATION),
// ...
]);
重用有状态节点访问者¶
将访问者附加到 \Twig\Environment
例如,Twig用它来访问 all 它编译的模板。如果您需要保留一些状态信息,您可能需要在访问新模板时重置它。
这可以通过以下代码实现:
protected $someTemplateState = [];
public function enterNode(\Twig\Node\Node $node, \Twig\Environment $env)
{
if ($node instanceof \Twig\Node\ModuleNode) {
// reset the state as we are entering a new template
$this->someTemplateState = [];
}
// ...
return $node;
}
使用数据库存储模板¶
如果您正在开发CMS,模板通常存储在数据库中。这个配方提供了一个简单的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
扩展名):
1 | {{ include(template_from_string("Hello {{ name }}")) }}
|
在PHP中,还可以通过 \Twig\Environment::createTemplate()
::
$template = $twig->createTemplate('hello {{ name }}');
echo $template->render(['name' => 'Fabien']);
在相同的模板中使用Twig和AngularJS¶
不建议在同一个文件中混合使用不同的模板语法,因为AngularJS和Twig在其语法中使用相同的分隔符: {{{{
和 }}}}
.
不过,如果要在同一个模板中使用AngularJS和Twig,根据需要包含在模板中的AngularJS的数量,有两种方法可以使其工作:
通过将AngularJS节包装为
{{% verbatim %}}
标记或通过转义每个分隔符{{{{ '{{{{' }}}}
和{{{{ '}}}}' }}}}
;更改其中一个模板引擎的分隔符(取决于上次引入的引擎):
对于AngularJS,使用
interpolateProvider
服务,例如在模块初始化时:1 2 3
angular.module('myApp', []).config(function($interpolateProvider) { $interpolateProvider.startSymbol('{[').endSymbol(']}'); });
对于Twig,通过
tag_variable
Lexer选项:$env->setLexer(new \Twig\Lexer($env, [ 'tag_variable' => ['{[', ']}'], ]));