伸长的树枝¶
Twig可以以多种方式扩展;您可以添加额外的标记、过滤器、测试、运算符、全局变量和函数。您甚至可以使用节点访问者扩展解析器本身。
注解
本章第一节介绍如何扩展Twig。如果您想在不同的项目中重用您的更改,或者希望与其他人共享这些更改,那么您应该创建一个扩展,如以下部分所述。
警告
在不创建扩展的情况下扩展Twig时,当PHP代码更新时,Twig将无法重新编译模板。要实时查看更改,请禁用模板缓存或将代码打包到扩展中(请参阅本章下一节)。
在扩展Twig之前,必须了解所有可能的扩展点之间的差异以及何时使用它们。
首先,请记住,Twig有两个主要的语言结构:
{{{{ }}}}
:用于打印表达式求值的结果;{{% %}}
:用于执行语句。
为了理解Twig为什么会公开这么多扩展点,让我们看看如何实现 乱数假文 需要知道生成器的字数。
You can use a lipsum
tag:
1 | {% lipsum 40 %}
|
这是可行的,但是使用标签 lipsum
不是一个好主意至少有三个主要原因:
lipsum
不是语言结构;标签输出一些东西;
标记不灵活,因为不能在表达式中使用它:
1
{{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}
实际上,您很少需要创建标记;这是一个好消息,因为标记是最复杂的扩展点。
Now, let's use a lipsum
filter:
1 | {{ 40|lipsum }}
|
同样,它也起作用了。但是过滤器应该将传递的值转换为其他值。这里,我们使用值来表示要生成的单词数(因此, 40
是筛选器的参数,而不是要转换的值)。
Next, let's use a lipsum
function:
1 | {{ lipsum(40) }}
|
我们开始吧。对于这个特定的例子,函数的创建是要使用的扩展点。您可以在任何接受表达式的地方使用它:
1 2 3 | {{ 'some text' ~ lipsum(40) ~ 'some more text' }}
{% set lipsum = lipsum(40) %}
|
最后,您还可以使用 全球的 对象的方法可以生成lorem ipsum文本:
1 | {{ text.lipsum(40) }}
|
作为一个经验法则,函数用于常用的特性,全局对象用于其他所有功能。
要延伸Twig时,请记住以下几点:
什么? | 实施困难? | 多久? | 什么时候? |
---|---|---|---|
宏 | 简单的 | 频繁的 | 内容生成 |
全球的 | 简单的 | 频繁的 | 辅助对象 |
功能 | 简单的 | 频繁的 | 内容生成 |
滤波器 | 简单的 | 频繁的 | 价值转换 |
tag | 复杂的 | 稀有的 | DSL语言构造 |
test | 简单的 | 稀有的 | 布尔决策 |
操作人员 | 简单的 | 稀有的 | 价值观转换 |
全球性的¶
全局变量与任何其他模板变量一样,但它在所有模板和宏中都可用:
$twig = new \Twig\Environment($loader);
$twig->addGlobal('text', new Text());
然后您可以使用 text
模板中任意位置的变量:
1 | {{ text.lipsum(40) }}
|
过滤器¶
创建过滤器包括将一个名称与一个可调用的PHP相关联:
// an anonymous function
$filter = new \Twig\TwigFilter('rot13', function ($string) {
return str_rot13($string);
});
// or a simple PHP function
$filter = new \Twig\TwigFilter('rot13', 'str_rot13');
// or a class static method
$filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);
$filter = new \Twig\TwigFilter('rot13', 'SomeClass::rot13Filter');
// or a class method
$filter = new \Twig\TwigFilter('rot13', [$this, 'rot13Filter']);
// the one below needs a runtime implementation (see below for more information)
$filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);
传递给 \Twig\TwigFilter
constructor是您将在模板中使用的过滤器的名称,第二个是可以与之关联的PHP调用。
然后,将过滤器添加到Twig环境中:
$twig = new \Twig\Environment($loader);
$twig->addFilter($filter);
下面是如何在模板中使用它:
1 2 3 | {{ 'Twig'|rot13 }}
{# will output Gjvt #}
|
当被Twig调用时,PHP callable接收过滤器的左侧(在管道之前) |
)作为第一个参数和传递给过滤器的额外参数(在括号内 ()
)作为额外的论据。
例如,以下代码:
1 2 | {{ 'TWIG'|lower }}
{{ now|date('d/m/Y') }}
|
编译为如下内容:
<?php echo strtolower('TWIG') ?>
<?php echo twig_date_format_filter($now, 'd/m/Y') ?>
这个 \Twig\TwigFilter
类的最后一个参数是:
$filter = new \Twig\TwigFilter('rot13', 'str_rot13', $options);
环境感知过滤器¶
如果要访问筛选器中的当前环境实例,请设置 needs_environment
选择权 true
;Twig将当前环境作为筛选器调用的第一个参数传递:
$filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $string) {
// get the current charset for instance
$charset = $env->getCharset();
return str_rot13($string);
}, ['needs_environment' => true]);
上下文感知过滤器¶
如果要访问筛选器中的当前上下文,请设置 needs_context
选择权 true
;Twig将当前上下文作为第一个参数传递给filter调用(如果 needs_environment
也设置为 true
):
$filter = new \Twig\TwigFilter('rot13', function ($context, $string) {
// ...
}, ['needs_context' => true]);
$filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $context, $string) {
// ...
}, ['needs_context' => true, 'needs_environment' => true]);
自动逃逸¶
如果启用了自动转义,则可以在打印之前转义过滤器的输出。如果过滤器充当转义器(或显式输出HTML或JavaScript代码),则需要打印原始输出。在这种情况下,设置 is_safe
选项:
$filter = new \Twig\TwigFilter('nl2br', 'nl2br', ['is_safe' => ['html']]);
有些过滤器可能需要处理已经转义或安全的输入,例如,当向原来不安全的输出添加(安全)HTML标记时。在这种情况下,设置 pre_escape
在输入数据通过筛选器之前对其进行转义的选项:
$filter = new \Twig\TwigFilter('somefilter', 'somefilter', ['pre_escape' => 'html', 'is_safe' => ['html']]);
可变滤波器¶
当筛选器应接受任意数量的参数时,设置 is_variadic
选择权 true
;Twig将把额外的参数作为最后一个参数作为数组传递给filter调用:
$filter = new \Twig\TwigFilter('thumbnail', function ($file, array $options = []) {
// ...
}, ['is_variadic' => true]);
请注意 named arguments 无法检查传递给变量筛选器的有效性,因为它们将自动结束在选项数组中。
动态滤波器¶
包含特殊 *
字符是一个动态过滤器 *
部件将匹配任何字符串:
$filter = new \Twig\TwigFilter('*_path', function ($name, $arguments) {
// ...
});
以下过滤器与上述定义的动态过滤器相匹配:
product_path
category_path
动态过滤器可以定义多个动态部件:
$filter = new \Twig\TwigFilter('*_path_*', function ($name, $suffix, $arguments) {
// ...
});
过滤器在普通过滤器参数之前,但在环境和上下文之后接收所有动态部分值。例如,调用 'foo'|a_path_b()
将导致以下参数传递给筛选器: ('a', 'b', 'foo')
.
功能¶
函数的定义方式与过滤器完全相同,但您需要创建 \Twig\TwigFunction
::
$twig = new \Twig\Environment($loader);
$function = new \Twig\TwigFunction('function_name', function () {
// ...
});
$twig->addFunction($function);
函数支持与过滤器相同的功能,除了 pre_escape
和 preserves_safety
选项。
测验¶
测试的定义方式与过滤器和函数的定义方式完全相同,但您需要创建 \Twig\TwigTest
::
$twig = new \Twig\Environment($loader);
$test = new \Twig\TwigTest('test_name', function () {
// ...
});
$twig->addTest($test);
计算应用程序的布尔值以允许您创建特定的逻辑条件。作为一个简单的例子,让我们创建一个Twig测试来检查对象是否为“红色”:
$twig = new \Twig\Environment($loader);
$test = new \Twig\TwigTest('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中的测试所使用的:
namespace App;
use Twig\Environment;
use Twig\Node\Expression\TestExpression;
use Twig\TwigTest;
$twig = new Environment($loader);
$test = new TwigTest(
'odd',
null,
['node_class' => OddTestExpression::class]);
$twig->addTest($test);
class OddTestExpression extends TestExpression
{
public function compile(\Twig\Compiler $compiler)
{
$compiler
->raw('(')
->subcompile($this->getNode('node'))
->raw(' % 2 != 0')
->raw(')')
;
}
}
上面的示例演示了如何创建使用节点类的测试。node类可以访问一个子节点 node
. 包含被测试子节点的值。当 odd
过滤器用于以下代码中:
1 | {% if my_value is odd %}
|
这个 node
子节点将包含一个表达式 my_value
. 基于节点的测试还可以访问 arguments
节点。此节点将包含已提供给测试的各种其他参数。
如果要将可变数量的位置参数或命名参数传递给测试,请设置 is_variadic
选择权 true
. 测试支持动态名称(有关语法,请参阅动态筛选器)。
标签¶
像Twig这样的模板引擎最令人兴奋的特性之一是可以定义新的 语言结构 . 这也是最复杂的特性,因为您需要了解Twig的内部结构是如何工作的。
但大多数情况下,不需要标记:
如果标记生成一些输出,请使用 功能 相反。
如果标记修改某些内容并返回它,请使用 滤波器 相反。
例如,如果要创建一个将降价格式文本转换为HTML的标记,请创建一个
markdown
而是过滤:1
{{ '**markdown** text'|markdown }}
如果要对大量文本使用此筛选器,请使用 apply 标签:
1 2 3 4 5 6
{% apply markdown %} Title ===== Much better than creating a tag as you can **compose** filters. {% endapply %}
如果您的标记不输出任何内容,但只是因为副作用而存在,请创建一个 功能 它什么也不返回,并通过 filter 标签。
例如,如果要创建一个记录文本的标记,则创建一个
log
函数,并通过 do 标签:1
{% do log('Log some things') %}
如果你仍然想为新的语言构造创建标签,那太好了!
让我们创建一个 set
允许从模板中定义简单变量的标记。标签的用法如下:
1 2 3 4 5 | {% set name = "value" %}
{{ name }}
{# should output value #}
|
注解
这个 set
标记是核心扩展的一部分,因此始终可用。内置版本的功能稍强一些,默认情况下支持多个分配。
定义新标记需要三个步骤:
- 定义令牌解析器类(负责解析模板代码);
- 定义一个节点类(负责将解析后的代码转换成PHP);
- 正在注册标记。
注册新标签¶
通过调用 addTokenParser
方法在 \Twig\Environment
实例:
$twig = new \Twig\Environment($loader);
$twig->addTokenParser(new Project_Set_TokenParser());
定义令牌解析器¶
现在,让我们看看这个类的实际代码:
class Project_Set_TokenParser extends \Twig\TokenParser\AbstractTokenParser
{
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()
方法在解析器遇到 set
标签。它应该返回一个 \Twig\Node\Node
表示节点(的 Project_Set_Node
调用创建将在下一节中进行说明)。
由于可以从令牌流调用一系列方法,解析过程得以简化 ($this->parser->getStream()
):
getCurrent()
:获取流中的当前令牌。next()
:移动到流中的下一个标记, 但还是老的 .test($type)
,test($value)
或test($type, $value)
:确定当前令牌是特定类型还是值(或两者都是)。该值可以是多个可能值的数组。expect($type[, $value[, $message]])
:如果当前标记不是给定的类型/值,则引发语法错误。否则,如果类型和值正确,则返回令牌并将流移到下一个令牌。look()
:查看下一个令牌而不使用它。
通过调用 parseExpression()
就像我们为 set
标签。
小技巧
阅读现有的 TokenParser
类是学习解析过程所有基本细节的最佳方法。
定义节点¶
这个 Project_Set_Node
课程本身很短:
class Project_Set_Node extends \Twig\Node\Node
{
public function __construct($name, \Twig\Node\Expression\AbstractExpression $value, $line, $tag = null)
{
parent::__construct(['value' => $value], ['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\ForNode
作为用法示例)。addDebugInfo()
:将与当前节点相关的原始模板文件的行添加为注释。indent()
:缩进生成的代码(请参见\Twig\Node\BlockNode
作为用法示例)。outdent()
:输出生成的代码(请参见\Twig\Node\BlockNode
作为用法示例)。
创建扩展¶
编写扩展的主要动机是将经常使用的代码移到可重用的类中,比如添加对国际化的支持。扩展可以定义标记、过滤器、测试、运算符、函数和节点访问者。
大多数时候,为您的项目创建一个单独的扩展名是很有用的,它可以承载所有要添加到Twig中的特定标记和过滤器。
小技巧
在将代码打包到扩展中时,Twig足够聪明,只要您对模板进行更改(当 auto_reload
已启用)。
扩展是实现以下接口的类:
interface \Twig\Extension\ExtensionInterface
{
/**
* Returns the token parser instances to add to the existing list.
*
* @return \Twig\TokenParser\TokenParserInterface[]
*/
public function getTokenParsers();
/**
* Returns the node visitor instances to add to the existing list.
*
* @return \Twig\NodeVisitor\NodeVisitorInterface[]
*/
public function getNodeVisitors();
/**
* Returns a list of filters to add to the existing list.
*
* @return \Twig\TwigFilter[]
*/
public function getFilters();
/**
* Returns a list of tests to add to the existing list.
*
* @return \Twig\TwigTest[]
*/
public function getTests();
/**
* Returns a list of functions to add to the existing list.
*
* @return \Twig\TwigFunction[]
*/
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\AbstractExtension
类,而不是实现接口,因为它为所有方法提供空实现:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
}
这个扩展目前没有任何作用。我们将在下一节中对其进行定制。
您可以将扩展保存在文件系统的任何位置,因为所有扩展都必须显式注册才能在模板中使用。
您可以使用 addExtension()
方法 Environment
对象:
$twig = new \Twig\Environment($loader);
$twig->addExtension(new Project_Twig_Extension());
小技巧
Twig核心扩展是扩展如何工作的很好的例子。
全球性的¶
全局变量可以通过 getGlobals()
方法:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface
{
public function getGlobals(): array
{
return [
'text' => new Text(),
];
}
// ...
}
功能¶
函数可以通过 getFunctions()
方法:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
public function getFunctions()
{
return [
new \Twig\TwigFunction('lipsum', 'generate_lipsum'),
];
}
// ...
}
过滤器¶
你需要重写一个过滤器扩展 getFilters()
方法。此方法必须返回要添加到Twig环境的筛选器数组:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
public function getFilters()
{
return [
new \Twig\TwigFilter('rot13', 'str_rot13'),
];
}
// ...
}
标签¶
在扩展中添加标记可以通过重写 getTokenParsers()
方法。此方法必须返回要添加到Twig环境的标记数组:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
public function getTokenParsers()
{
return [new Project_Set_TokenParser()];
}
// ...
}
在上面的代码中,我们添加了一个新标记,由 Project_Set_TokenParser
班级。这个 Project_Set_TokenParser
类负责解析标记并将其编译为PHP。
算子¶
这个 getOperators()
方法允许您添加新运算符。下面是如何添加 !
, ||
和 &&
操作员:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
public function getOperators()
{
return [
[
'!' => ['precedence' => 50, 'class' => \Twig\Node\Expression\Unary\NotUnary::class],
],
[
'||' => ['precedence' => 10, 'class' => \Twig\Node\Expression\Binary\OrBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
'&&' => ['precedence' => 15, 'class' => \Twig\Node\Expression\Binary\AndBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
],
];
}
// ...
}
测验¶
这个 getTests()
方法允许您添加新的测试函数::
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
public function getTests()
{
return [
new \Twig\TwigTest('even', 'twig_test_even'),
];
}
// ...
}
定义与运行时¶
Twig过滤器、函数和测试运行时实现可以定义为任何有效的PHP可调用:
- functions/static methods :实现简单且快速(所有Twig核心扩展都使用);但运行时很难依赖外部对象;
- 关闭 :易于实现;
- 对象方法 :如果运行时代码依赖于外部对象,则更加灵活和必需。
使用方法的最简单方法是在扩展本身上定义它们:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
private $rot13Provider;
public function __construct($rot13Provider)
{
$this->rot13Provider = $rot13Provider;
}
public function getFunctions()
{
return [
new \Twig\TwigFunction('rot13', [$this, 'rot13']),
];
}
public function rot13($value)
{
return $this->rot13Provider->rot13($value);
}
}
这非常方便,但不建议这样做,因为它使模板编译依赖于运行时依赖项,即使不需要它们(例如,可以将其看作是连接到数据库引擎的依赖项)。
您可以通过注册 \Twig\RuntimeLoader\RuntimeLoaderInterface
环境上的实例,该实例知道如何实例化此类运行时类(运行时类必须是可自动加载的):
class RuntimeLoader implements \Twig\RuntimeLoader\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\RuntimeLoader\ContainerRuntimeLoader
)
现在可以将运行时逻辑移到新的 Project_Twig_RuntimeExtension
类并直接在扩展中使用它:
class Project_Twig_RuntimeExtension
{
private $rot13Provider;
public function __construct($rot13Provider)
{
$this->rot13Provider = $rot13Provider;
}
public function rot13($value)
{
return $this->rot13Provider->rot13($value);
}
}
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
public function getFunctions()
{
return [
new \Twig\TwigFunction('rot13', ['Project_Twig_RuntimeExtension', 'rot13']),
// or
new \Twig\TwigFunction('rot13', 'Project_Twig_RuntimeExtension::rot13'),
];
}
}
测试扩展¶
功能测试¶
通过在测试目录中创建以下文件结构,可以为扩展创建功能测试:
Fixtures/
filters/
foo.test
bar.test
functions/
foo.test
bar.test
tags/
foo.test
bar.test
IntegrationTest.php
这个 IntegrationTest.php
文件应如下所示:
use Twig\Test\IntegrationTestCase;
class Project_Tests_IntegrationTest extends IntegrationTestCase
{
public function getExtensions()
{
return [
new Project_Twig_Extension1(),
new Project_Twig_Extension2(),
];
}
public function getFixturesDir()
{
return __DIR__.'/Fixtures/';
}
}
fixture示例可以在Twig存储库中找到 tests/Twig/Fixtures 目录。
节点测试¶
测试节点访问者可能很复杂,因此从 \Twig\Test\NodeTestCase
. 示例可以在Twig存储库中找到 tests/Twig/Node 目录。