> For the complete documentation index, see [llms.txt](https://twig.shujuwajue.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://twig.shujuwajue.com/twig-2x/kuo-zhan-twig.md).

# 扩展 Twig

> 原文：[Extending Twig](https://twig.sensiolabs.org/doc/2.x/advanced.html)\
> 翻译：小虾米（QQ:509129）

## 扩展 Twig

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

> 本章的第一部分描述了如何轻松地扩展Twig。如果您想要在不同的项目中重用您的变更，或者您想要与其他项目共享它们，那么您应该在下面的小节中创建一个扩展。
>
> 在不创建扩展的情况下扩展Twig时，当PHP代码更新时，Twig将无法重新编译您的模板。要实时查看更改，要么禁用模板缓存，要么将代码打包为扩展(参见本章的下一节)。

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

首先，记住Twig有两种主要的语言结构:

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

为了理解为什么Twig暴露了这么多的扩展点，让我们看看如何实现一个Lorem ipsum生成器(它需要知道生成的单词的数量)。

您可以使用lipsum标签:

```markup
{% lipsum 40 %}
```

这是可行的，但是用一个标签来表示lipsum并不是一个好主意，至少有三个主要原因:

* lipsum不是一种语言结构;
* 标签输出的东西;
* 标签不灵活，因为您不能在表达式中使用它:

```markup
{{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}
```

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

现在，让我们用一个lipsum过滤器:

```markup
{{ 40|lipsum }}
```

同样的，它是可行的，但它看起来很奇怪。过滤器将传递的值转换为其他东西，但是这里我们使用值来表示生成的单词的数量(因此，40是过滤器的参数，而不是我们想要转换的值)。

接下来，让我们使用一个lipsum函数:

```markup
{{ lipsum(40) }}
```

我们开始吧。对于这个特定的示例，创建函数是使用的扩展点。你可以在任何地方使用这个表达方式:

```markup
{{ 'some text' ~ lipsum(40) ~ 'some more text' }}

{% set lipsum = lipsum(40) %}
```

最后，您还可以使用一个全局对象，使用一个能够生成lorem ipsum文本的方法:

```markup
{{ text.lipsum(40) }}
```

作为经验法则，使用函数来频繁地使用特性和全局对象。

当你想扩展Twig时，请记住以下几点:

|      什么？      | 实施困难吗？ | 多久一次 |   什么时候？  |
| :-----------: | :----: | :--: | :------: |
|    宏（macro）   |    不   |  频繁  |   内容生成   |
|  全局变量（global） |    不   |  频繁  | Helper对象 |
|  函数（function） |    不   |  频繁  |   内容生成   |
|  过滤器（filter）  |    不   |  频繁  |    值转换   |
|    标签器（tag）   |   复杂   |  稀有  |  DSL语言结构 |
|   测试器（test）   |    不   |  稀有  |   布尔判定   |
| 操作符(operator) |    不   |  稀有  |    值转换   |

### 全局变量(Globals)

```php
$twig = new Twig_Environment($loader);
$twig->addGlobal('text', new Text());
```

然后可以在模板中的任何地方使用文本变量:

```markup
{{ text.lipsum(40) }}
```

### 过滤器(Filters)

创建过滤器器就像将名称与PHP可调用的名称关联一样简单:

```php
// an anonymous function
$filter = new Twig_Filter('rot13', function ($string) {
    return str_rot13($string);
});

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

// or a class static method
$filter = new Twig_Filter('rot13', array('SomeClass', 'rot13Filter'));
$filter = new Twig_Filter('rot13', 'SomeClass::rot13Filter');

// or a class method
$filter = new Twig_Filter('rot13', array($this, 'rot13Filter'));
// the one below needs a runtime implementation (see below for more information)
$filter = new Twig_Filter('rot13', array('SomeClass', 'rot13Filter'));
```

传递给Twig\_Filter构造函数的第一个参数是您将在模板中使用的过滤器的名称，第二个参数是与它关联的PHP。\
然后，将过滤器添加到您的Twig环境中:

```php
$twig = new Twig_Environment($loader);
$twig->addFilter($filter);
```

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

```markup
{{ 'Twig'|rot13 }}

{# will output Gjvt #}
```

当被Twig调用时，PHP callable将过滤器的左侧(在管道|之前)作为第一个参数，并且将额外的参数传递给过滤器(在括号()中)作为额外的参数。\
例如，以下代码:

```markup
{{ 'TWIG'|lower }}
{{ now|date('d/m/Y') }}
```

被编译成如下内容:

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

Twig\_Filter使用一个数组选项作为最后的参数:

```php
$filter = new Twig_Filter('rot13', 'str_rot13', $options);
```

#### Environment-aware过滤器(Environment-aware Filters)

如果您想访问过滤器中的当前环境实例，请将needs\_environment选项设置为true;Twig将把当前环境作为第一个参数传递给筛选器:

```php
$filter = new Twig_Filter('rot13', function (Twig_Environment $env, $string) {
    // get the current charset for instance
    $charset = $env->getCharset();

    return str_rot13($string);
}, array('needs_environment' => true));
```

#### Context-aware Filters(Context-aware过滤器)

如果您想要访问过滤器中的当前上下文，请将needs\_context选项设置为true;Twig将把当前上下文作为第一个参数传递到过滤器调用(或者第二个If needs\_environment也设置为true):

```php
$filter = new Twig_Filter('rot13', function ($context, $string) {
    // ...
}, array('needs_context' => true));

$filter = new Twig_Filter('rot13', function (Twig_Environment $env, $context, $string) {
    // ...
}, array('needs_context' => true, 'needs_environment' => true));
```

#### 自动转义(Automatic Escaping)

如果可以自动转义，那么过滤器的输出可能在打印之前就可以转义。如果您的过滤器充当了一个转义者(或者显式输出HTML或JavaScript代码)，那么您将希望打印原始输出。在这种情况下，设置is\_safe选项:

```php
$filter = new Twig_Filter('nl2br', 'nl2br', array('is_safe' => array('html')));
```

一些过滤器可能需要处理已经安全的输入，例如在添加(安全)HTML标签到最初不安全的输出时。在这种情况下，设置pre\_escape选项，在它运行通过过滤器之前，要转义输入数据:

```php
$filter = new Twig_Filter('somefilter', 'somefilter', array('pre_escape' => 'html', 'is_safe' => array('html')));
```

#### 可变的过滤器(Variadic Filters)

当一个过滤器应该接受任意数量的参数时，将is\_variadic选项设置为true;Twig将把额外的参数作为数组的最后一个参数传递给过滤器。

```php
$filter = new Twig_Filter('thumbnail', function ($file, array $options = array()) {
    // ...
}, array('is_variadic' => true));
```

请注意，传递给变量过滤器的命名参数不能检查有效性，它们会自动终止在选项数组中。

#### 动态过滤器(Dynamic Filters)

包含特殊\*字符的过滤器名称是一个动态过滤器，它可以是任意字符串:

```php
$filter = new Twig_Filter('*_path', function ($name, $arguments) {
    // ...
});
```

下面的过滤器将与上面定义的动态过滤器匹配:

* product\_path
* category\_path

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

```php
$filter = new Twig_Filter('*_path_*', function ($name, $suffix, $arguments) {
    // ...
});
```

过滤器将在正常的过滤器参数之前接收所有动态部件值，但是在环境和上下文之后。例如，对“foo”| a\_path\_b()的调用将导致将下列参数传递给过滤器:(“a”、“b”、“foo”)。

#### 弃用的过滤器(Deprecated Filters)

您可以通过将弃用选项设置为true来标记过滤器。您还可以提供一个替代的过滤器，它在有意义的情况下替代已弃用的过滤器:

```php
$filter = new Twig_Filter('obsolete', function () {
    // ...
}, array('deprecated' => true, 'alternative' => 'new_one'));
```

当一个过滤器被弃用时，Twig会在编译一个使用它的模板时发出一个弃用通知。有关更多信息，请参见显示[弃用通知](https://twig.sensiolabs.org/doc/2.x/recipes.html#deprecation-notices)。

### 函数(Functions)

函数的定义与过滤器完全相同，但您需要创建Twig\_Function的一个实例:

```php
$twig = new Twig_Environment($loader);
$function = new Twig_Function('function_name', function () {
    // ...
});
$twig->addFunction($function);
```

函数支持与过滤器相同的特性，除了pre\_escape和preserves\_safety选项之外。

测试器(Tests)

测试的定义与过滤器和函数的定义完全相同，但是您需要创建Twig\_Test的一个实例:

```php
$twig = new Twig_Environment($loader);
$test = new Twig_Test('test_name', function () {
    // ...
});
$twig->addTest($test);
```

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

```php
$twig = new Twig_Environment($loader);
$test = new Twig_Test('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中构建的测试所使用的:

```php
$twig = new Twig_Environment($loader);
$test = new Twig_Test(
    'odd',
    null,
    array('node_class' => 'Twig_Node_Expression_Test_Odd'));
$twig->addTest($test);

class Twig_Node_Expression_Test_Odd extends Twig_Node_Expression_Test
{
    public function compile(Twig_Compiler $compiler)
    {
        $compiler
            ->raw('(')
            ->subcompile($this->getNode('node'))
            ->raw(' % 2 == 1')
            ->raw(')')
        ;
    }
}
```

上面的示例演示如何创建使用节点类的测试。节点类可以访问一个叫“节点”的子节点。此子节点包含正在测试的值。当奇数过滤器用于代码，如：

```markup
{% if my_value is odd %}
```

节点子节点将包含my\_value的表达式。基于节点的测试还可以访问参数节点。这个节点将包含提供给您测试的各种其他参数。

如果您想要将变量的位置或命名参数传递给测试，那么将is\_variadic选项设置为true。测试还支持动态名称特性作为过滤器和函数。

### 标签器(Tags)

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

让我们创建一个简单的set标签，允许在模板内定义简单的变量。标签可以使用如下:

```markup
{% set name = "value" %}

{{ name }}

{# should output value #}
```

> set标记是核心扩展的一部分，因此总是可用的。内置版本的功能稍微强大一些，默认情况下支持多个赋值(cf.模板设计器章节以获取更多信息)。

定义一个新标签需要三个步骤:

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

#### 注册一个新的标签(Registering a new tag)

添加一个标记就像在Twig\_Environment实例上调用addTokenParser方法一样简单:

```php
$twig = new Twig_Environment($loader);
$twig->addTokenParser(new Project_Set_TokenParser());
```

#### 定义一个令牌解析器(Defining a Token Parser)

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

```php
class Project_Set_TokenParser extends Twig_TokenParser
{
    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()方法。它应该返回表示该节点的Twig\_Node实例(Project\_Set\_Node调用创建在下一节中解释)。

由于可以从令牌流中调用的一系列方法($ this - >解析器- > getStream())，解析过程简化了。

* getCurrent():获取流中的当前标记。
* next():移动到流中的下一个标记，但返回旧的。
* test($ type)、test($value)或test($type，$value):确定当前标记是否为特定类型或值(或两者)。值可能是几个可能值的数组。
* expect($type，$value\[$message]):如果当前的令牌不是给定的类型/值，则会抛出语法错误。否则，如果类型和值是正确的，则返回令牌并将流移动到下一个令牌。
* look():查看下一个令牌而不使用它。

解析表达式是通过调用parseExpression()来完成的，就像我们为set标签所做的那样。

> 阅读现有的TokenParser类是了解解析过程所有细节的最好方法。

#### 定义一个节点（Defining a Node）

Project\_Set\_Node类本身相当简单:

```php
class Project_Set_Node extends Twig_Node
{
    public function __construct($name, Twig_Node_Expression $value, $line, $tag = null)
    {
        parent::__construct(array('value' => $value), array('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\_For的使用示例)。
* addDebugInfo():添加与当前节点相关的原始模板文件的行作为注释。
* indent():对生成的代码进行缩进(参见Twig\_Node\_Block作为一个使用例)。
* outdent():输出生成的代码(参见Twig\_Node\_Block以获得使用示例)。

### 穿件一个扩展（Creating an Extension）

编写扩展的主要动机是将经常使用的代码转移到可重用的类中，例如为国际化添加支持。扩展可以定义标记、筛选器、测试、操作符、全局变量、函数和节点访问者。

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

> 当将代码打包为扩展时，Twig足够智能，可以在每次更改时重新编译您的模板(当启用auto\_reload时)。
>
> 在编写自己的扩展之前，请查看Twig官方扩展存储库:<http://github.com/twigphp/twig扩展。>

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

```php
interface Twig_ExtensionInterface
{
    /**
     * Returns the token parser instances to add to the existing list.
     *
     * @return Twig_TokenParserInterface[]
     */
    public function getTokenParsers();

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

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

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

    /**
     * Returns a list of functions to add to the existing list.
     *
     * @return Twig_Function[]
     */
    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类继承而不是实现接口，因为它为所有方法提供了空实现:

```php
class Project_Twig_Extension扩展了Twig_Extension { }
```

当然，这个扩展现在什么也不做。我们将在下一节中对其进行定制。

Twig不关心在文件系统中保存扩展的位置，因为所有扩展都必须在模板中显式注册。

您可以在主环境对象上使用addExtension()方法注册一个扩展:

```php
$twig = new Twig_Environment($loader);
$twig->addExtension(new Project_Twig_Extension());
```

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

#### 全局变量(Globals)

全局变量可以通过getGlobals()方法在扩展中注册:

```php
class Project_Twig_Extension extends Twig_Extension implements Twig_Extension_GlobalsInterface
{
    public function getGlobals()
    {
        return array(
            'text' => new Text(),
        );
    }

    // ...
}
```

#### 函数(Functions)

函数可以通过getFunctions()方法在扩展中注册:

```php
class Project_Twig_Extension extends Twig_Extension
{
    public function getFunctions()
    {
        return array(
            new Twig_Function('lipsum', 'generate_lipsum'),
        );
    }

    // ...
}
```

#### 过滤器(Filters)

要向扩展添加过滤器，您需要重写getfilter()方法。此方法必须返回一个过滤器数组，以添加到Twig环境:

```php
class Project_Twig_Extension extends Twig_Extension
{
    public function getFilters()
    {
        return array(
            new Twig_Filter('rot13', 'str_rot13'),
        );
    }

    // ...
}
```

#### 标签器(Tags)

在扩展中添加标记可以通过覆盖getTokenParsers()方法来完成。此方法必须返回一个标记数组，以添加到Twig环境:

```php
class Project_Twig_Extension extends Twig_Extension
{
    public function getTokenParsers()
    {
        return array(new Project_Set_TokenParser());
    }

    // ...
}
```

在上面的代码中，我们添加了一个新标签，由Project\_Set\_TokenParser类定义。Project\_Set\_TokenParser类负责解析标记并将其编译为PHP。

#### 操作符(Operators)

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

```php
class Project_Twig_Extension extends Twig_Extension
{
    public function getOperators()
    {
        return array(
            array(
                '!' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Not'),
            ),
            array(
                '||' => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
                '&&' => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
            ),
        );
    }

    // ...
}
```

#### 测试器(Tests)

getTests()方法让您添加新的测试函数:

```php
class Project_Twig_Extension extends Twig_Extension
{
    public function getTests()
    {
        return array(
            new Twig_Test('even', 'twig_test_even'),
        );
    }

    // ...
}
```

#### Definition vs Runtime

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

* 函数/静态方法(functions/static methods):简单实现和快速(由所有Twig核心扩展使用);但是运行时很难依赖于外部对象;
* 闭包(closures):简单的实现;
* 对象方法(object methods):如果运行时代码依赖于外部对象，则更加灵活和必需。

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

```php
class Project_Twig_Extension extends Twig_Extension
{
    private $rot13Provider;

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

    public function getFunctions()
    {
        return array(
            new Twig_Function('rot13', array($this, 'rot13')),
        );
    }

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

这是非常方便的，但不推荐，因为它使模板编译依赖于运行时依赖关系，即使它们不需要(例如作为连接到数据库引擎的依赖关系)。

您可以通过在环境中注册一个Twig\_RuntimeLoaderInterface实例来轻松地将扩展定义与运行时实现分离，该实例知道如何实例化这样的运行时类(运行时类必须是可自动的):

```php
class RuntimeLoader implements Twig_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\_ContainerRuntimeLoader)。

现在可以将运行时逻辑转移到一个新的Project\_Twig\_RuntimeExtension类，并直接在扩展中使用它:

```php
class Project_Twig_RuntimeExtension extends Twig_Extension
{
    private $rot13Provider;

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

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

class Project_Twig_Extension extends Twig_Extension
{
    public function getFunctions()
    {
        return array(
            new Twig_Function('rot13', array('Project_Twig_RuntimeExtension', 'rot13')),
            // or
            new Twig_Function('rot13', 'Project_Twig_RuntimeExtension::rot13'),
        );
    }
}
```

### 负载(Overloading)

为了超载已经定义的过滤器、测试、操作符、全局变量或函数，将其重新定义为扩展，并尽可能晚地注册(订单事项):

```php
class MyCoreExtension extends Twig_Extension
{
    public function getFilters()
    {
        return array(
            new Twig_Filter('date', array($this, 'dateFilter')),
        );
    }

    public function dateFilter($timestamp, $format = 'F j, Y H:i')
    {
        // do something different from the built-in date filter
    }
}

$twig = new Twig_Environment($loader);
$twig->addExtension(new MyCoreExtension());
```

在这里，我们已经使用自定义的日期过滤器重载了内置的日期过滤器。

如果您在Twig\_Environment上执行相同的操作，请注意它比任何其他注册扩展都优先:

```php
$twig = new Twig_Environment($loader);
$twig->addFilter(new Twig_Filter('date', function ($timestamp, $format = 'F j, Y H:i') {
    // do something different from the built-in date filter
}));
// the date filter will come from the above registration, not
// from the registered extension below
$twig->addExtension(new MyCoreExtension());
```

> 注意，不建议重写内置的Twig元素，因为它可能会令人困惑。

### 测试一个扩展(Testing an Extension)

#### 函数测试(Functional Tests)

通过在测试目录中创建以下文件结构，您可以为扩展创建函数测试:

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

IntegrationTest.php文件应该是这样的:

```php
class Project_Tests_IntegrationTest extends Twig_Test_IntegrationTestCase
{
    public function getExtensions()
    {
        return array(
            new Project_Twig_Extension1(),
            new Project_Twig_Extension2(),
        );
    }

    public function getFixturesDir()
    {
        return dirname(__FILE__).'/Fixtures/';
    }
}
```

可以在Twig仓库[tests/Twig/Fixtures](https://github.com/twigphp/Twig/tree/master/test/Twig/Tests/Fixtures)目录中找到fixture示例。

#### 节点测试(Node Tests)

测试节点访问者可能是复杂的，因此从Twig\_Test\_NodeTestCase扩展您的测试用例。示例可以在Twig仓库[tests/Twig/Node](https://github.com/twigphp/Twig/tree/master/test/Twig/Tests/Node)目录中找到。
