扩展 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
Was this helpful?