食谱

显示弃用通知

不推荐的功能生成弃用通知(通过调用 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.twigindex.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' => ['{[', ']}'],
      ]));