问:我正在使用Workspaces中的Twig模板语言。自从我开始使用Twig以来,服务器已经停止执行文档中包含的其他PHP代码。相反,它显示为HTML注释(在开发工具中可见)。除此之外,Slim路由和Twig模板功能似乎运行良好。我试图重新制作文件无济于事。关于为什么发生这种情况以及如何解决的任何想法?
答:我怀疑您不能在模板中使用常规php,因为现在树枝已经解析了它,而不是php。我的猜测是,当遇到php或未知部分时,必须将它们放在html注释中,这样才不会在页面或视图中出错。
在扩展Twig之前,您必须了解所有可能的扩展点之间的区别以及何时使用它们。
首先,请记住,Twig具有两种主要的语言构造:
{{ }}:用于打印表达式求值的结果;
{% %}:用于执行语句。
要了解Twig为什么公开了这么多扩展点,让我们看看如何实现Lorem ipsum生成器(它需要知道要生成的单词数)。
您可以使用lipsum 标签:
1
{% lipsum 40 %}
可以,但是lipsum至少出于以下三个主要原因,使用标记不是一个好主意:
lipsum 不是语言构造;
标签输出一些东西;
标签不灵活,因为您不能在表达式中使用它:
1
{{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}
实际上,您几乎不需要创建标签;这是个好消息,因为标记是最复杂的扩展点。
现在,让我们使用lipsum 过滤器:
1
{{ 40|lipsum }}
再次,它起作用。但是过滤器应该将传递的值转换为其他值。在这里,我们使用该值指示要生成的单词数(因此,它 40是过滤器的参数,而不是我们要转换的值)。
接下来,让我们使用一个lipsum 函数:
1
{{ lipsum(40) }}
开始了。对于此特定示例,创建功能是要使用的扩展点。您可以在任何接受表达式的地方使用它:
1
2
3
{{ 'some text' ~ lipsum(40) ~ 'some more text' }}
{% set lipsum = lipsum(40) %}
最后,您还可以将全局对象与能够生成lorem ipsum文本的方法一起使用:
1
{{ text.lipsum(40) }}
根据经验,将函数用于常用功能,将全局对象用于其他所有功能。
要扩展Twig时请记住以下几点:
什么?实施困难?多常?什么时候?
巨集简单的经常内容生成
全球的简单的经常辅助对象
功能简单的经常内容生成
筛选简单的经常价值转换
标签复杂的稀有的DSL语言构造
测试简单的稀有的布尔决策
操作员简单的稀有的价值转型
全局变量
全局变量与任何其他模板变量一样,除了它在所有模板和宏中均可用:
$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构造函数的第一个参数是您将在模板中使用的过滤器的名称,第二个参数是可调用的与之关联的PHP。
然后,将过滤器添加到Twig环境:
$twig = new \Twig\Environment($loader);
$twig->addFilter($filter);
这里是如何在模板中使用它:
1
2
3
{{ 'Twig'|rot13 }}
{# will output Gjvt #}
当Twig调用时,PHP可调用对象将过滤器的左侧(在pipe之前|)作为第一个参数,并将传递到过滤器的额外参数(在括号内())作为额外参数。
例如,以下代码:
1
2
{{ 'TWIG'|lower }}
{{ now|date('d/m/Y') }}
被编译为以下内容:
该\Twig\TwigFilter班采取的选项,其最后一个参数数组:
$filter = new \Twig\TwigFilter('rot13', 'str_rot13', $options);
环保过滤器¶
如果要访问过滤器中的当前环境实例,请将needs_environment选项设置 为true;。Twig将当前环境作为第一个参数传递给filter调用:
$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]);
请注意,无法检查传递给可变参数过滤器的命名参数的有效性,因为它们将自动出现在选项数组中。
动态筛选¶
包含特殊*字符的过滤器名称是动态过滤器,该*部分将与任何字符串匹配:
$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')
不推荐使用的过滤器¶
您可以通过将deprecated选项设置为来将过滤器标记为已弃用true。您还可以提供另一种过滤器,该过滤器将在有意义的情况下替换不推荐使用的过滤器:
$filter = new \Twig\TwigFilter('obsolete', function () {
// ...
}, ['deprecated' => true, 'alternative' => 'new_one']);
不赞成使用过滤器时,Twig在使用它编译模板时会发出不赞成使用的通知。有关更多信息,请参见显示弃用通知。
功能¶
函数的定义与过滤器完全相同,但是您需要创建以下实例\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。该子节点包含正在测试的值。在以下odd代码中使用过滤器时:
1
{% if my_value is odd %}
该node子节点将包含的表达my_value。基于节点的测试也可以访问该arguments节点。该节点将包含已提供给测试的各种其他参数。
如果要将可变数量的位置或命名参数传递给测试,请将is_variadic选项设置为true。测试支持动态名称(有关语法,请参见动态过滤器)。
标签¶
像Twig这样的模板引擎最令人兴奋的功能之一就是可以定义新的语言结构。这也是最复杂的功能,因为您需要了解Twig内部的工作方式。
不过,在大多数情况下,不需要标签:
如果您的标签产生了一些输出,请改用一个函数。
如果您的代码修改了某些内容并返回了该内容,请改用过滤器。
例如,如果要创建将Markdown格式的文本转换为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标签是核心延伸的一部分,因此始终可用。内置版本功能更强大,默认情况下支持多种分配。
定义新标签需要三个步骤:
定义令牌解析器类(负责解析模板代码);
定义Node类(负责将解析的代码转换为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 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:
功能/静态方法:易于实现且快速(所有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/';
}
}
灯具示例可在Twig存储库 tests / Twig / Fixtures目录中找到。
节点测试¶
测试节点访问者可能很复杂,因此请从扩展测试用例 \Twig\Test\NodeTestCase。可以在Twig存储库tests / Twig / Node目录中找到示例 。