伸长的树枝

Twig可以以多种方式扩展;您可以添加额外的标记、过滤器、测试、运算符、全局变量和函数。您甚至可以使用节点访问者扩展解析器本身。

注解

本章第一节介绍如何扩展Twig。如果您想在不同的项目中重用您的更改,或者希望与其他人共享这些更改,那么您应该创建一个扩展,如以下部分所述。

警告

在不创建扩展的情况下扩展Twig时,当PHP代码更新时,Twig将无法重新编译模板。要实时查看更改,请禁用模板缓存或将代码打包到扩展中(请参阅本章下一节)。

在扩展Twig之前,必须了解所有可能的扩展点之间的差异以及何时使用它们。

首先,请记住,Twig有两个主要的语言结构:

  • {{{{ }}}} :用于打印表达式求值的结果;
  • {{% %}} :用于执行语句。

为了理解Twig为什么会公开这么多扩展点,让我们看看如何实现 乱数假文 需要知道生成器的字数。

You can use a lipsum tag:

1
{% lipsum 40 %}

这是可行的,但是使用标签 lipsum 不是一个好主意至少有三个主要原因:

  • lipsum 不是语言结构;

  • 标签输出一些东西;

  • 标记不灵活,因为不能在表达式中使用它:

    1
    {{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}
    

实际上,您很少需要创建标记;这是一个好消息,因为标记是最复杂的扩展点。

Now, let's use a lipsum filter:

1
{{ 40|lipsum }}

同样,它也起作用了。但是过滤器应该将传递的值转换为其他值。这里,我们使用值来表示要生成的单词数(因此, 40 是筛选器的参数,而不是要转换的值)。

Next, let's use a lipsum function:

1
{{ lipsum(40) }}

我们开始吧。对于这个特定的例子,函数的创建是要使用的扩展点。您可以在任何接受表达式的地方使用它:

1
2
3
{{ 'some text' ~ lipsum(40) ~ 'some more text' }}

{% set lipsum = lipsum(40) %}

最后,您还可以使用 全球的 对象的方法可以生成lorem ipsum文本:

1
{{ text.lipsum(40) }}

作为一个经验法则,函数用于常用的特性,全局对象用于其他所有功能。

要延伸Twig时,请记住以下几点:

什么? 实施困难? 多久? 什么时候?
简单的 频繁的 内容生成
全球的 简单的 频繁的 辅助对象
功能 简单的 频繁的 内容生成
滤波器 简单的 频繁的 价值转换
tag 复杂的 稀有的 DSL语言构造
test 简单的 稀有的 布尔决策
操作人员 简单的 稀有的 价值观转换

全球性的

全局变量与任何其他模板变量一样,但它在所有模板和宏中都可用:

$twig = new \Twig\Environment($loader);
$twig->addGlobal('text', new Text());

然后您可以使用 text 模板中任意位置的变量:

1
{{ text.lipsum(40) }}

过滤器

创建过滤器包括将一个名称与一个可调用的PHP相关联:

// an anonymous function
$filter = new \Twig\TwigFilter('rot13', function ($string) {
    return str_rot13($string);
});

// or a simple PHP function
$filter = new \Twig\TwigFilter('rot13', 'str_rot13');

// or a class static method
$filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);
$filter = new \Twig\TwigFilter('rot13', 'SomeClass::rot13Filter');

// or a class method
$filter = new \Twig\TwigFilter('rot13', [$this, 'rot13Filter']);
// the one below needs a runtime implementation (see below for more information)
$filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);

传递给 \Twig\TwigFilter constructor是您将在模板中使用的过滤器的名称,第二个是可以与之关联的PHP调用。

然后,将过滤器添加到Twig环境中:

$twig = new \Twig\Environment($loader);
$twig->addFilter($filter);

下面是如何在模板中使用它:

1
2
3
{{ 'Twig'|rot13 }}

{# will output Gjvt #}

当被Twig调用时,PHP callable接收过滤器的左侧(在管道之前) | )作为第一个参数和传递给过滤器的额外参数(在括号内 () )作为额外的论据。

例如,以下代码:

1
2
{{ 'TWIG'|lower }}
{{ now|date('d/m/Y') }}

编译为如下内容:

<?php echo strtolower('TWIG') ?>
<?php echo twig_date_format_filter($now, 'd/m/Y') ?>

这个 \Twig\TwigFilter 类的最后一个参数是:

$filter = new \Twig\TwigFilter('rot13', 'str_rot13', $options);

环境感知过滤器

如果要访问筛选器中的当前环境实例,请设置 needs_environment 选择权 true ;Twig将当前环境作为筛选器调用的第一个参数传递:

$filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $string) {
    // get the current charset for instance
    $charset = $env->getCharset();

    return str_rot13($string);
}, ['needs_environment' => true]);

上下文感知过滤器

如果要访问筛选器中的当前上下文,请设置 needs_context 选择权 true ;Twig将当前上下文作为第一个参数传递给filter调用(如果 needs_environment 也设置为 true ):

$filter = new \Twig\TwigFilter('rot13', function ($context, $string) {
    // ...
}, ['needs_context' => true]);

$filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $context, $string) {
    // ...
}, ['needs_context' => true, 'needs_environment' => true]);

自动逃逸

如果启用了自动转义,则可以在打印之前转义过滤器的输出。如果过滤器充当转义器(或显式输出HTML或JavaScript代码),则需要打印原始输出。在这种情况下,设置 is_safe 选项:

$filter = new \Twig\TwigFilter('nl2br', 'nl2br', ['is_safe' => ['html']]);

有些过滤器可能需要处理已经转义或安全的输入,例如,当向原来不安全的输出添加(安全)HTML标记时。在这种情况下,设置 pre_escape 在输入数据通过筛选器之前对其进行转义的选项:

$filter = new \Twig\TwigFilter('somefilter', 'somefilter', ['pre_escape' => 'html', 'is_safe' => ['html']]);

可变滤波器

当筛选器应接受任意数量的参数时,设置 is_variadic 选择权 true ;Twig将把额外的参数作为最后一个参数作为数组传递给filter调用:

$filter = new \Twig\TwigFilter('thumbnail', function ($file, array $options = []) {
    // ...
}, ['is_variadic' => true]);

请注意 named arguments 无法检查传递给变量筛选器的有效性,因为它们将自动结束在选项数组中。

动态滤波器

包含特殊 * 字符是一个动态过滤器 * 部件将匹配任何字符串:

$filter = new \Twig\TwigFilter('*_path', function ($name, $arguments) {
    // ...
});

以下过滤器与上述定义的动态过滤器相匹配:

  • product_path
  • category_path

动态过滤器可以定义多个动态部件:

$filter = new \Twig\TwigFilter('*_path_*', function ($name, $suffix, $arguments) {
    // ...
});

过滤器在普通过滤器参数之前,但在环境和上下文之后接收所有动态部分值。例如,调用 'foo'|a_path_b() 将导致以下参数传递给筛选器: ('a', 'b', 'foo') .

不推荐使用的筛选器

通过设置 deprecated 选择权 true . 您还可以提供一个替代筛选器,在有意义时替换不推荐的筛选器:

$filter = new \Twig\TwigFilter('obsolete', function () {
    // ...
}, ['deprecated' => true, 'alternative' => 'new_one']);

当过滤器被弃用时,Twig在使用它编译模板时发出一个弃用通知。看到了吗 显示弃用通知 更多信息。

功能

函数的定义方式与过滤器完全相同,但您需要创建 \Twig\TwigFunction ::

$twig = new \Twig\Environment($loader);
$function = new \Twig\TwigFunction('function_name', function () {
    // ...
});
$twig->addFunction($function);

函数支持与过滤器相同的功能,除了 pre_escapepreserves_safety 选项。

测验

测试的定义方式与过滤器和函数的定义方式完全相同,但您需要创建 \Twig\TwigTest ::

$twig = new \Twig\Environment($loader);
$test = new \Twig\TwigTest('test_name', function () {
    // ...
});
$twig->addTest($test);

计算应用程序的布尔值以允许您创建特定的逻辑条件。作为一个简单的例子,让我们创建一个Twig测试来检查对象是否为“红色”:

$twig = new \Twig\Environment($loader);
$test = new \Twig\TwigTest('red', function ($value) {
    if (isset($value->color) && $value->color == 'red') {
        return true;
    }
    if (isset($value->paint) && $value->paint == 'red') {
        return true;
    }
    return false;
});
$twig->addTest($test);

测试函数必须始终返回 true/false .

创建测试时,可以使用 node_class 提供自定义测试编译的选项。如果您的测试可以编译成PHP原语,这将非常有用。这是许多内置在Twig中的测试所使用的:

namespace App;

use Twig\Environment;
use Twig\Node\Expression\TestExpression;
use Twig\TwigTest;

$twig = new Environment($loader);
$test = new TwigTest(
    'odd',
    null,
    ['node_class' => OddTestExpression::class]);
$twig->addTest($test);

class OddTestExpression extends TestExpression
{
    public function compile(\Twig\Compiler $compiler)
    {
        $compiler
            ->raw('(')
            ->subcompile($this->getNode('node'))
            ->raw(' % 2 != 0')
            ->raw(')')
        ;
    }
}

上面的示例演示了如何创建使用节点类的测试。node类可以访问一个子节点 node . 包含被测试子节点的值。当 odd 过滤器用于以下代码中:

1
{% if my_value is odd %}

这个 node 子节点将包含一个表达式 my_value . 基于节点的测试还可以访问 arguments 节点。此节点将包含已提供给测试的各种其他参数。

如果要将可变数量的位置参数或命名参数传递给测试,请设置 is_variadic 选择权 true . 测试支持动态名称(有关语法,请参阅动态筛选器)。

标签

像Twig这样的模板引擎最令人兴奋的特性之一是可以定义新的 语言结构 . 这也是最复杂的特性,因为您需要了解Twig的内部结构是如何工作的。

但大多数情况下,不需要标记:

  • 如果标记生成一些输出,请使用 功能 相反。

  • 如果标记修改某些内容并返回它,请使用 滤波器 相反。

    例如,如果要创建一个将降价格式文本转换为HTML的标记,请创建一个 markdown 而是过滤:

    1
    {{ '**markdown** text'|markdown }}
    

    如果要对大量文本使用此筛选器,请使用 apply 标签:

    1
    2
    3
    4
    5
    6
    {% apply markdown %}
    Title
    =====
    
    Much better than creating a tag as you can **compose** filters.
    {% endapply %}
    
  • 如果您的标记不输出任何内容,但只是因为副作用而存在,请创建一个 功能 它什么也不返回,并通过 filter 标签。

    例如,如果要创建一个记录文本的标记,则创建一个 log 函数,并通过 do 标签:

    1
    {% do log('Log some things') %}
    

如果你仍然想为新的语言构造创建标签,那太好了!

让我们创建一个 set 允许从模板中定义简单变量的标记。标签的用法如下:

1
2
3
4
5
{% set name = "value" %}

{{ name }}

{# should output value #}

注解

这个 set 标记是核心扩展的一部分,因此始终可用。内置版本的功能稍强一些,默认情况下支持多个分配。

定义新标记需要三个步骤:

  • 定义令牌解析器类(负责解析模板代码);
  • 定义一个节点类(负责将解析后的代码转换成PHP);
  • 正在注册标记。

注册新标签

通过调用 addTokenParser 方法在 \Twig\Environment 实例:

$twig = new \Twig\Environment($loader);
$twig->addTokenParser(new Project_Set_TokenParser());

定义令牌解析器

现在,让我们看看这个类的实际代码:

class Project_Set_TokenParser extends \Twig\TokenParser\AbstractTokenParser
{
    public function parse(\Twig\Token $token)
    {
        $parser = $this->parser;
        $stream = $parser->getStream();

        $name = $stream->expect(\Twig\Token::NAME_TYPE)->getValue();
        $stream->expect(\Twig\Token::OPERATOR_TYPE, '=');
        $value = $parser->getExpressionParser()->parseExpression();
        $stream->expect(\Twig\Token::BLOCK_END_TYPE);

        return new Project_Set_Node($name, $value, $token->getLine(), $this->getTag());
    }

    public function getTag()
    {
        return 'set';
    }
}

这个 getTag() 方法必须返回我们要解析的标记 set .

这个 parse() 方法在解析器遇到 set 标签。它应该返回一个 \Twig\Node\Node 表示节点(的 Project_Set_Node 调用创建将在下一节中进行说明)。

由于可以从令牌流调用一系列方法,解析过程得以简化 ($this->parser->getStream() ):

  • getCurrent() :获取流中的当前令牌。
  • next() :移动到流中的下一个标记, 但还是老的 .
  • test($type)test($value)test($type, $value) :确定当前令牌是特定类型还是值(或两者都是)。该值可以是多个可能值的数组。
  • expect($type[, $value[, $message]]) :如果当前标记不是给定的类型/值,则引发语法错误。否则,如果类型和值正确,则返回令牌并将流移到下一个令牌。
  • look() :查看下一个令牌而不使用它。

通过调用 parseExpression() 就像我们为 set 标签。

小技巧

阅读现有的 TokenParser 类是学习解析过程所有基本细节的最佳方法。

定义节点

这个 Project_Set_Node 课程本身很短:

class Project_Set_Node extends \Twig\Node\Node
{
    public function __construct($name, \Twig\Node\Expression\AbstractExpression $value, $line, $tag = null)
    {
        parent::__construct(['value' => $value], ['name' => $name], $line, $tag);
    }

    public function compile(\Twig\Compiler $compiler)
    {
        $compiler
            ->addDebugInfo($this)
            ->write('$context[\''.$this->getAttribute('name').'\'] = ')
            ->subcompile($this->getNode('value'))
            ->raw(";\n")
        ;
    }
}

编译器实现了一个流畅的接口,并提供了一些方法来帮助开发人员生成漂亮且可读的PHP代码:

  • subcompile() :编译节点。
  • raw() :按原样写入给定字符串。
  • write() :通过在每行开头添加缩进来写入给定字符串。
  • string() :写入带引号的字符串。
  • repr() :编写给定值的PHP表示(请参见 \Twig\Node\ForNode 作为用法示例)。
  • addDebugInfo() :将与当前节点相关的原始模板文件的行添加为注释。
  • indent() :缩进生成的代码(请参见 \Twig\Node\BlockNode 作为用法示例)。
  • outdent() :输出生成的代码(请参见 \Twig\Node\BlockNode 作为用法示例)。

创建扩展

编写扩展的主要动机是将经常使用的代码移到可重用的类中,比如添加对国际化的支持。扩展可以定义标记、过滤器、测试、运算符、函数和节点访问者。

大多数时候,为您的项目创建一个单独的扩展名是很有用的,它可以承载所有要添加到Twig中的特定标记和过滤器。

小技巧

在将代码打包到扩展中时,Twig足够聪明,只要您对模板进行更改(当 auto_reload 已启用)。

扩展是实现以下接口的类:

interface \Twig\Extension\ExtensionInterface
{
    /**
     * Returns the token parser instances to add to the existing list.
     *
     * @return \Twig\TokenParser\TokenParserInterface[]
     */
    public function getTokenParsers();

    /**
     * Returns the node visitor instances to add to the existing list.
     *
     * @return \Twig\NodeVisitor\NodeVisitorInterface[]
     */
    public function getNodeVisitors();

    /**
     * Returns a list of filters to add to the existing list.
     *
     * @return \Twig\TwigFilter[]
     */
    public function getFilters();

    /**
     * Returns a list of tests to add to the existing list.
     *
     * @return \Twig\TwigTest[]
     */
    public function getTests();

    /**
     * Returns a list of functions to add to the existing list.
     *
     * @return \Twig\TwigFunction[]
     */
    public function getFunctions();

    /**
     * Returns a list of operators to add to the existing list.
     *
     * @return array<array> First array of unary operators, second array of binary operators
     */
    public function getOperators();
}

要保持扩展类的干净和精简,请从内置 \Twig\Extension\AbstractExtension 类,而不是实现接口,因为它为所有方法提供空实现:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
}

这个扩展目前没有任何作用。我们将在下一节中对其进行定制。

您可以将扩展保存在文件系统的任何位置,因为所有扩展都必须显式注册才能在模板中使用。

您可以使用 addExtension() 方法 Environment 对象:

$twig = new \Twig\Environment($loader);
$twig->addExtension(new Project_Twig_Extension());

小技巧

Twig核心扩展是扩展如何工作的很好的例子。

全球性的

全局变量可以通过 getGlobals() 方法:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface
{
    public function getGlobals(): array
    {
        return [
            'text' => new Text(),
        ];
    }

    // ...
}

功能

函数可以通过 getFunctions() 方法:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getFunctions()
    {
        return [
            new \Twig\TwigFunction('lipsum', 'generate_lipsum'),
        ];
    }

    // ...
}

过滤器

你需要重写一个过滤器扩展 getFilters() 方法。此方法必须返回要添加到Twig环境的筛选器数组:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getFilters()
    {
        return [
            new \Twig\TwigFilter('rot13', 'str_rot13'),
        ];
    }

    // ...
}

标签

在扩展中添加标记可以通过重写 getTokenParsers() 方法。此方法必须返回要添加到Twig环境的标记数组:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getTokenParsers()
    {
        return [new Project_Set_TokenParser()];
    }

    // ...
}

在上面的代码中,我们添加了一个新标记,由 Project_Set_TokenParser 班级。这个 Project_Set_TokenParser 类负责解析标记并将其编译为PHP。

算子

这个 getOperators() 方法允许您添加新运算符。下面是如何添加 !||&& 操作员:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getOperators()
    {
        return [
            [
                '!' => ['precedence' => 50, 'class' => \Twig\Node\Expression\Unary\NotUnary::class],
            ],
            [
                '||' => ['precedence' => 10, 'class' => \Twig\Node\Expression\Binary\OrBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
                '&&' => ['precedence' => 15, 'class' => \Twig\Node\Expression\Binary\AndBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
            ],
        ];
    }

    // ...
}

测验

这个 getTests() 方法允许您添加新的测试函数::

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getTests()
    {
        return [
            new \Twig\TwigTest('even', 'twig_test_even'),
        ];
    }

    // ...
}

定义与运行时

Twig过滤器、函数和测试运行时实现可以定义为任何有效的PHP可调用:

  • functions/static methods :实现简单且快速(所有Twig核心扩展都使用);但运行时很难依赖外部对象;
  • 关闭 :易于实现;
  • 对象方法 :如果运行时代码依赖于外部对象,则更加灵活和必需。

使用方法的最简单方法是在扩展本身上定义它们:

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    private $rot13Provider;

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

    public function getFunctions()
    {
        return [
            new \Twig\TwigFunction('rot13', [$this, 'rot13']),
        ];
    }

    public function rot13($value)
    {
        return $this->rot13Provider->rot13($value);
    }
}

这非常方便,但不建议这样做,因为它使模板编译依赖于运行时依赖项,即使不需要它们(例如,可以将其看作是连接到数据库引擎的依赖项)。

您可以通过注册 \Twig\RuntimeLoader\RuntimeLoaderInterface 环境上的实例,该实例知道如何实例化此类运行时类(运行时类必须是可自动加载的):

class RuntimeLoader implements \Twig\RuntimeLoader\RuntimeLoaderInterface
{
    public function load($class)
    {
        // implement the logic to create an instance of $class
        // and inject its dependencies
        // most of the time, it means using your dependency injection container
        if ('Project_Twig_RuntimeExtension' === $class) {
            return new $class(new Rot13Provider());
        } else {
            // ...
        }
    }
}

$twig->addRuntimeLoader(new RuntimeLoader());

注解

Twig附带了一个PSR-11兼容的运行时加载器 (\Twig\RuntimeLoader\ContainerRuntimeLoader

现在可以将运行时逻辑移到新的 Project_Twig_RuntimeExtension 类并直接在扩展中使用它:

class Project_Twig_RuntimeExtension
{
    private $rot13Provider;

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

    public function rot13($value)
    {
        return $this->rot13Provider->rot13($value);
    }
}

class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
    public function getFunctions()
    {
        return [
            new \Twig\TwigFunction('rot13', ['Project_Twig_RuntimeExtension', 'rot13']),
            // or
            new \Twig\TwigFunction('rot13', 'Project_Twig_RuntimeExtension::rot13'),
        ];
    }
}

测试扩展

功能测试

通过在测试目录中创建以下文件结构,可以为扩展创建功能测试:

Fixtures/
    filters/
        foo.test
        bar.test
    functions/
        foo.test
        bar.test
    tags/
        foo.test
        bar.test
IntegrationTest.php

这个 IntegrationTest.php 文件应如下所示:

use Twig\Test\IntegrationTestCase;

class Project_Tests_IntegrationTest extends IntegrationTestCase
{
    public function getExtensions()
    {
        return [
            new Project_Twig_Extension1(),
            new Project_Twig_Extension2(),
        ];
    }

    public function getFixturesDir()
    {
        return __DIR__.'/Fixtures/';
    }
}

fixture示例可以在Twig存储库中找到 tests/Twig/Fixtures 目录。

节点测试

测试节点访问者可能很复杂,因此从 \Twig\Test\NodeTestCase . 示例可以在Twig存储库中找到 tests/Twig/Node 目录。