食谱

显示弃用通知

不推荐的功能生成弃用通知(通过调用 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发出请求并相应地选择布局时:

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

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

使Include动态

包含模板时,其名称不必是字符串。例如,名称可以依赖于变量的值:

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

如果 var 评估为 index , the index_foo.html 将呈现模板。

实际上,模板名称可以是任何有效的表达式,例如:

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

重写同时扩展自身的模板

可以通过两种不同的方式自定义模板:

  • 遗传 :模板 延伸 父模板和覆盖一些块;

  • 替换 :如果使用文件系统加载器,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 在我们的案子里。这将使系统中的每个模板文件都具有唯一的可寻址性。大多数时候您都会使用“正常”路径,但是在特殊情况下,如果希望扩展模板,且模板本身具有覆盖版本,我们可以在扩展标记中引用其父级的完整、明确的模板路径:

{# 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'],
    ],
];

以及以下模板以显示所有主题中的所有消息:

{% 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

In the inner loop, the loop.parent variable is used to access the outer context. So, the index of the current topic defined in the outer for loop is accessible via the loop.parent.loop.index variable.

动态定义未定义的函数和过滤器

当函数(或过滤器)未定义时,Twig默认抛出 \Twig\Error\SyntaxError 例外情况。但是,它也可以调用 callback (任何有效的PHP可调用)应该返回函数(或过滤器)。

对于过滤器,使用 registerUndefinedFilterCallback() . 对于函数,请使用 registerUndefinedFunctionCallback() ::

// auto-register all native PHP functions as Twig functions
// don't try this at home as it's not secure at all!
$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 扩展名):

{{ 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 服务,例如在模块初始化时:

      angular.module('myApp', []).config(function($interpolateProvider) {
          $interpolateProvider.startSymbol('{[').endSymbol(']}');
      });
      
    • 对于Twig,通过 tag_variable Lexer选项:

      $env->setLexer(new \Twig\Lexer($env, [
          'tag_variable' => ['{[', ']}'],
      ]));