# 扩展 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)目录中找到。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://twig.shujuwajue.com/twig-2x/kuo-zhan-twig.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
