扩展 Twig

原文:Extending Twig 翻译:小虾米(QQ:509129)

扩展 Twig

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

本章的第一部分描述了如何轻松地扩展Twig。如果您想要在不同的项目中重用您的变更,或者您想要与其他项目共享它们,那么您应该在下面的小节中创建一个扩展。

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

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

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

  • {{ }}: 用于打印表达式的结果;

  • {% %}: 用于执行语句。

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

您可以使用lipsum标签:

{% lipsum 40 %}

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

  • lipsum不是一种语言结构;

  • 标签输出的东西;

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

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

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

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

{{ 40|lipsum }}

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

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

{{ lipsum(40) }}

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

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

{% set lipsum = lipsum(40) %}

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

{{ text.lipsum(40) }}

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

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

什么?

实施困难吗?

多久一次

什么时候?

宏(macro)

频繁

内容生成

全局变量(global)

频繁

Helper对象

函数(function)

频繁

内容生成

过滤器(filter)

频繁

值转换

标签器(tag)

复杂

稀有

DSL语言结构

测试器(test)

稀有

布尔判定

操作符(operator)

稀有

值转换

全局变量(Globals)

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

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

{{ text.lipsum(40) }}

过滤器(Filters)

创建过滤器器就像将名称与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环境中:

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

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

{{ 'Twig'|rot13 }}

{# will output Gjvt #}

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

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

被编译成如下内容:

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

Twig_Filter使用一个数组选项作为最后的参数:

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

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

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

$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):

$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选项:

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

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

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

可变的过滤器(Variadic Filters)

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

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

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

动态过滤器(Dynamic Filters)

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

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

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

  • product_path

  • category_path

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

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

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

弃用的过滤器(Deprecated Filters)

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

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

当一个过滤器被弃用时,Twig会在编译一个使用它的模板时发出一个弃用通知。有关更多信息,请参见显示弃用通知

函数(Functions)

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

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

函数支持与过滤器相同的特性,除了pre_escape和preserves_safety选项之外。

测试器(Tests)

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

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

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

$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中构建的测试所使用的:

$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(')')
        ;
    }
}

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

{% if my_value is odd %}

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

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

标签器(Tags)

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

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

{% set name = "value" %}

{{ name }}

{# should output value #}

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

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

  • 定义令牌解析器类(负责解析模板代码);

  • 定义一个节点类(负责将解析的代码转换为PHP);

  • 注册标记。

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

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

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

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

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

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类本身相当简单:

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扩展。

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

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类继承而不是实现接口,因为它为所有方法提供了空实现:

class Project_Twig_Extension扩展了Twig_Extension { }

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

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

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

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

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

全局变量(Globals)

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

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

    // ...
}

函数(Functions)

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

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

    // ...
}

过滤器(Filters)

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

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

    // ...
}

标签器(Tags)

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

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()方法允许添加新操作符。下面是如何添加!| |,和& &运算符:

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()方法让您添加新的测试函数:

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):如果运行时代码依赖于外部对象,则更加灵活和必需。

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

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实例来轻松地将扩展定义与运行时实现分离,该实例知道如何实例化这样的运行时类(运行时类必须是可自动的):

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类,并直接在扩展中使用它:

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)

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

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上执行相同的操作,请注意它比任何其他注册扩展都优先:

$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)

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

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

IntegrationTest.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目录中找到fixture示例。

节点测试(Node Tests)

测试节点访问者可能是复杂的,因此从Twig_Test_NodeTestCase扩展您的测试用例。示例可以在Twig仓库tests/Twig/Node目录中找到。

Last updated