原文:Extending Twig
翻译:小虾米(QQ:509129)
扩展 Twig
Twig可以以多种方式扩展;您可以添加额外的标记、过滤器、测试、操作符、全局变量和函数。甚至可以将解析器本身扩展到节点访问者。
本章的第一部分描述了如何轻松地扩展Twig。如果您想要在不同的项目中重用您的变更,或者您想要与其他项目共享它们,那么您应该在下面的小节中创建一个扩展。
在不创建扩展的情况下扩展Twig时,当PHP代码更新时,Twig将无法重新编译您的模板。要实时查看更改,要么禁用模板缓存,要么将代码打包为扩展(参见本章的下一节)。
在扩展Twig之前,您必须了解所有可能的扩展点和何时使用它们之间的差异。
首先,记住Twig有两种主要的语言结构:
为了理解为什么Twig暴露了这么多的扩展点,让我们看看如何实现一个Lorem ipsum生成器(它需要知道生成的单词的数量)。
您可以使用lipsum标签:
这是可行的,但是用一个标签来表示lipsum并不是一个好主意,至少有三个主要原因:
Copy {{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}
实际上,您很少需要创建标记;这是好消息,因为标签是Twig最复杂的扩展点。
现在,让我们用一个lipsum过滤器:
同样的,它是可行的,但它看起来很奇怪。过滤器将传递的值转换为其他东西,但是这里我们使用值来表示生成的单词的数量(因此,40是过滤器的参数,而不是我们想要转换的值)。
接下来,让我们使用一个lipsum函数:
我们开始吧。对于这个特定的示例,创建函数是使用的扩展点。你可以在任何地方使用这个表达方式:
Copy {{ 'some text' ~ lipsum(40) ~ 'some more text' }}
{% set lipsum = lipsum(40) %}
最后,您还可以使用一个全局对象,使用一个能够生成lorem ipsum文本的方法:
Copy {{ text.lipsum(40) }}
作为经验法则,使用函数来频繁地使用特性和全局对象。
当你想扩展Twig时,请记住以下几点:
全局变量(Globals)
Copy $twig = new Twig_Environment ($loader);
$twig -> addGlobal ( 'text' , new Text () ) ;
然后可以在模板中的任何地方使用文本变量:
Copy {{ text.lipsum(40) }}
过滤器(Filters)
创建过滤器器就像将名称与PHP可调用的名称关联一样简单:
Copy // 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环境中:
Copy $twig = new Twig_Environment ($loader);
$twig -> addFilter ( $filter ) ;
下面是如何在模板中使用它:
Copy {{ 'Twig'|rot13 }}
{# will output Gjvt #}
当被Twig调用时,PHP callable将过滤器的左侧(在管道|之前)作为第一个参数,并且将额外的参数传递给过滤器(在括号()中)作为额外的参数。
例如,以下代码:
Copy {{ 'TWIG'|lower }}
{{ now|date('d/m/Y') }}
被编译成如下内容:
Copy <? php echo strtolower ( 'TWIG' ) ?>
<? php echo twig_date_format_filter ( $now , 'd/m/Y' ) ?>
Twig_Filter使用一个数组选项作为最后的参数:
Copy $filter = new Twig_Filter ( 'rot13' , 'str_rot13' , $options);
Environment-aware过滤器(Environment-aware Filters)
如果您想访问过滤器中的当前环境实例,请将needs_environment选项设置为true;Twig将把当前环境作为第一个参数传递给筛选器:
Copy $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):
Copy $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选项:
Copy $filter = new Twig_Filter ( 'nl2br' , 'nl2br' , array ( 'is_safe' => array ( 'html' )));
一些过滤器可能需要处理已经安全的输入,例如在添加(安全)HTML标签到最初不安全的输出时。在这种情况下,设置pre_escape选项,在它运行通过过滤器之前,要转义输入数据:
Copy $filter = new Twig_Filter ( 'somefilter' , 'somefilter' , array ( 'pre_escape' => 'html' , 'is_safe' => array ( 'html' )));
可变的过滤器(Variadic Filters)
当一个过滤器应该接受任意数量的参数时,将is_variadic选项设置为true;Twig将把额外的参数作为数组的最后一个参数传递给过滤器。
Copy $filter = new Twig_Filter ( 'thumbnail' , function ($file , array $options = array ()) {
// ...
} , array ( 'is_variadic' => true ));
请注意,传递给变量过滤器的命名参数不能检查有效性,它们会自动终止在选项数组中。
动态过滤器(Dynamic Filters)
包含特殊*字符的过滤器名称是一个动态过滤器,它可以是任意字符串:
Copy $filter = new Twig_Filter ( '*_path' , function ($name , $arguments) {
// ...
});
下面的过滤器将与上面定义的动态过滤器匹配:
动态过滤器可以定义多个动态部件:
Copy $filter = new Twig_Filter ( '*_path_*' , function ($name , $suffix , $arguments) {
// ...
});
过滤器将在正常的过滤器参数之前接收所有动态部件值,但是在环境和上下文之后。例如,对“foo”| a_path_b()的调用将导致将下列参数传递给过滤器:(“a”、“b”、“foo”)。
弃用的过滤器(Deprecated Filters)
您可以通过将弃用选项设置为true来标记过滤器。您还可以提供一个替代的过滤器,它在有意义的情况下替代已弃用的过滤器:
Copy $filter = new Twig_Filter ( 'obsolete' , function () {
// ...
} , array ( 'deprecated' => true , 'alternative' => 'new_one' ));
当一个过滤器被弃用时,Twig会在编译一个使用它的模板时发出一个弃用通知。有关更多信息,请参见显示弃用通知 。
函数(Functions)
函数的定义与过滤器完全相同,但您需要创建Twig_Function的一个实例:
Copy $twig = new Twig_Environment ($loader);
$function = new Twig_Function ( 'function_name' , function () {
// ...
});
$twig -> addFunction ( $function ) ;
函数支持与过滤器相同的特性,除了pre_escape和preserves_safety选项之外。
测试器(Tests)
测试的定义与过滤器和函数的定义完全相同,但是您需要创建Twig_Test的一个实例:
Copy $twig = new Twig_Environment ($loader);
$test = new Twig_Test ( 'test_name' , function () {
// ...
});
$twig -> addTest ( $test ) ;
测试允许您创建自定义应用程序特定逻辑来评估布尔条件。作为一个简单的例子,让我们创建一个Twig测试来检查对象是否为“红色”:
Copy $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中构建的测试所使用的:
Copy $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 ( ')' )
;
}
}
上面的示例演示如何创建使用节点类的测试。节点类可以访问一个叫“节点”的子节点。此子节点包含正在测试的值。当奇数过滤器用于代码,如:
Copy {% if my_value is odd %}
节点子节点将包含my_value的表达式。基于节点的测试还可以访问参数节点。这个节点将包含提供给您测试的各种其他参数。
如果您想要将变量的位置或命名参数传递给测试,那么将is_variadic选项设置为true。测试还支持动态名称特性作为过滤器和函数。
标签器(Tags)
像Twig这样的模板引擎最令人兴奋的特性之一是定义新语言结构的可能性。这也是最复杂的特性,因为您需要了解Twig的内部工作是如何工作的。
让我们创建一个简单的set标签,允许在模板内定义简单的变量。标签可以使用如下:
Copy {% set name = "value" %}
{{ name }}
{# should output value #}
set标记是核心扩展的一部分,因此总是可用的。内置版本的功能稍微强大一些,默认情况下支持多个赋值(cf.模板设计器章节以获取更多信息)。
定义一个新标签需要三个步骤:
注册一个新的标签(Registering a new tag)
添加一个标记就像在Twig_Environment实例上调用addTokenParser方法一样简单:
Copy $twig = new Twig_Environment ($loader);
$twig -> addTokenParser ( new Project_Set_TokenParser () ) ;
定义一个令牌解析器(Defining a Token Parser)
现在,让我们看看这个类的实际代码:
Copy 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()),解析过程简化了。
next():移动到流中的下一个标记,但返回旧的。
test($ type)、test($value)或test($type,$value):确定当前标记是否为特定类型或值(或两者)。值可能是几个可能值的数组。
expect($type,$value[$message]):如果当前的令牌不是给定的类型/值,则会抛出语法错误。否则,如果类型和值是正确的,则返回令牌并将流移动到下一个令牌。
解析表达式是通过调用parseExpression()来完成的,就像我们为set标签所做的那样。
阅读现有的TokenParser类是了解解析过程所有细节的最好方法。
定义一个节点(Defining a Node)
Project_Set_Node类本身相当简单:
Copy 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代码:
write():在每一行的开头添加缩进来写给定的字符串。
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扩展。
扩展是实现以下接口的类:
Copy 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类继承而不是实现接口,因为它为所有方法提供了空实现:
Copy class Project_Twig_Extension扩展了Twig_Extension { }
当然,这个扩展现在什么也不做。我们将在下一节中对其进行定制。
Twig不关心在文件系统中保存扩展的位置,因为所有扩展都必须在模板中显式注册。
您可以在主环境对象上使用addExtension()方法注册一个扩展:
Copy $twig = new Twig_Environment ($loader);
$twig -> addExtension ( new Project_Twig_Extension () ) ;
Twig核心扩展是扩展工作的很好的例子。
全局变量(Globals)
全局变量可以通过getGlobals()方法在扩展中注册:
Copy class Project_Twig_Extension extends Twig_Extension implements Twig_Extension_GlobalsInterface
{
public function getGlobals ()
{
return array (
'text' => new Text () ,
);
}
// ...
}
函数(Functions)
函数可以通过getFunctions()方法在扩展中注册:
Copy class Project_Twig_Extension extends Twig_Extension
{
public function getFunctions ()
{
return array (
new Twig_Function ( 'lipsum' , 'generate_lipsum' ) ,
);
}
// ...
}
过滤器(Filters)
要向扩展添加过滤器,您需要重写getfilter()方法。此方法必须返回一个过滤器数组,以添加到Twig环境:
Copy class Project_Twig_Extension extends Twig_Extension
{
public function getFilters ()
{
return array (
new Twig_Filter ( 'rot13' , 'str_rot13' ) ,
);
}
// ...
}
标签器(Tags)
在扩展中添加标记可以通过覆盖getTokenParsers()方法来完成。此方法必须返回一个标记数组,以添加到Twig环境:
Copy 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()方法允许添加新操作符。下面是如何添加!| |,和& &运算符:
Copy 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()方法让您添加新的测试函数:
Copy 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核心扩展使用);但是运行时很难依赖于外部对象;
对象方法(object methods):如果运行时代码依赖于外部对象,则更加灵活和必需。
使用方法的最简单的方法是在扩展本身上定义它们:
Copy 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实例来轻松地将扩展定义与运行时实现分离,该实例知道如何实例化这样的运行时类(运行时类必须是可自动的):
Copy 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类,并直接在扩展中使用它:
Copy 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)
为了超载已经定义的过滤器、测试、操作符、全局变量或函数,将其重新定义为扩展,并尽可能晚地注册(订单事项):
Copy 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上执行相同的操作,请注意它比任何其他注册扩展都优先:
Copy $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)
通过在测试目录中创建以下文件结构,您可以为扩展创建函数测试:
Copy Fixtures /
filters /
foo . test
bar . test
functions /
foo . test
bar . test
tags /
foo . test
bar . test
IntegrationTest . php
IntegrationTest.php文件应该是这样的:
Copy 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 目录中找到。