我正在尝试创建一个自定义的Twig标签,例如:
{% mytag 'foo','bar' %}
Hello world!!
{% endmytag %}
此标记应打印my func("Hello world!!", "foo", "bar")
的输出。
有人可以发布一些示例代码来创建这样的自定义标记吗?可以接受任意数量的参数,我会更感激。
注意:我对创建自定义函数不感兴趣,我需要将标记的主体作为第一个参数传入。
答案 0 :(得分:75)
在谈论标签之前,您应该了解Twig如何在内部工作。
首先,由于Twig代码可以放在文件,字符串甚至数据库上,Twig会打开并使用Loader读取您的流。大多数已知加载器Twig_Loader_Filesystem
用于打开文件中的twig代码,而Twig_Loader_Array
用于直接从字符串中获取twig代码。
然后,解析此树枝代码以构建一个解析树,其中包含树枝代码的对象表示。每个对象都称为Node
,因为它们是树的一部分。与其他语言一样,Twig由标记组成,例如{%
,{#
,function()
,"string"
...因此,Twig语言结构将读取多个标记以构建正确的节点。
然后遍历解析树,并编译成PHP代码。生成的PHP类遵循Twig_Template
接口,因此呈现器可以调用该类的doDisplay
方法来生成最终结果。
如果您启用了缓存,则可以查看这些生成的文件并了解正在进行的操作。
所有内部树枝标记,例如{% block %}
,{% set %}
...都是使用与自定义标记相同的界面开发的,因此如果您需要某些特定样本,可以查看Twig源代码。
但是,你想要的样本无论如何都是一个好的开始,所以让我们开发吧。
令牌解析器的目标是解析和验证您的标记参数。例如,{% macro %}
标记需要一个名称,如果您给出一个字符串,它将崩溃。
当Twig找到一个标记时,它将查看TokenParser
方法返回的标记名称的所有已注册getTag()
类。如果名称匹配,则Twig调用该类的parse()
方法。
调用parse()
时,流指针仍在标记名称标记上。因此,我们应该获取所有内联参数,并通过查找BLOCK_END_TYPE
标记来完成标记声明。然后,我们对标签的主体进行子搜索(标签内包含的内容,因为它也可能包含树枝逻辑,例如标签和其他内容):每次新标签时都会调用decideMyTagFork
方法在body中找到:如果返回true,将破坏子解析。请注意,此方法名称不参与界面,这只是Twig内置扩展程序中使用的标准。
作为参考,Twig令牌可以是:
EOF_TYPE
:流的最后一个标记,表示结束。
TEXT_TYPE
:不参与twig语言的文本:例如,在Twig代码Hello, {{ var }}
中,hello,
是TEXT_TYPE
令牌。
BLOCK_START_TYPE
:"开始执行语句"令牌,{%
VAR_START_TYPE
:"开始获得表达结果"令牌,{{
BLOCK_END_TYPE
:"完成执行语句"令牌,%}
VAR_END_TYPE
:"完成表达结果"令牌,}}
NAME_TYPE
:此令牌就像一个没有引号的字符串,就像树枝中的变量名一样,{{ i_am_a_name_type }}
NUMBER_TYPE
:此类型的节点包含数字,例如3,-2,4.5 ......
STRING_TYPE
:包含一个用引号或双引号封装的字符串,例如'foo'
和"bar"
OPERATOR_TYPE
:包含一个运算符,例如+
,-
,还有~
,?
......您将永远不会需要此令牌,因为Twig已经提供了表达式解析器。
INTERPOLATION_START_TYPE
,"开始插值"令牌(因为Twig> = 1.5),插值是树枝字符串内的表达式解释,例如"my string, my #{variable} and 1+1 = #{1+1}"
。插值的开始是#{
。
INTERPOLATION_END_TYPE
," end interpolation"令牌(因为Twig> = 1.5),例如在插值打开时字符串内没有转义}
。
MyTagTokenParser.php
<?php
class MyTagTokenParser extends \Twig_TokenParser
{
public function parse(\Twig_Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
// recovers all inline parameters close to your tag name
$params = array_merge(array (), $this->getInlineParams($token));
$continue = true;
while ($continue)
{
// create subtree until the decideMyTagFork() callback returns true
$body = $this->parser->subparse(array ($this, 'decideMyTagFork'));
// I like to put a switch here, in case you need to add middle tags, such
// as: {% mytag %}, {% nextmytag %}, {% endmytag %}.
$tag = $stream->next()->getValue();
switch ($tag)
{
case 'endmytag':
$continue = false;
break;
default:
throw new \Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "endmytag" to close the "mytag" block started at line %d)', $lineno), -1);
}
// you want $body at the beginning of your arguments
array_unshift($params, $body);
// if your endmytag can also contains params, you can uncomment this line:
// $params = array_merge($params, $this->getInlineParams($token));
// and comment this one:
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
}
return new MyTagNode(new \Twig_Node($params), $lineno, $this->getTag());
}
/**
* Recovers all tag parameters until we find a BLOCK_END_TYPE ( %} )
*
* @param \Twig_Token $token
* @return array
*/
protected function getInlineParams(\Twig_Token $token)
{
$stream = $this->parser->getStream();
$params = array ();
while (!$stream->test(\Twig_Token::BLOCK_END_TYPE))
{
$params[] = $this->parser->getExpressionParser()->parseExpression();
}
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
return $params;
}
/**
* Callback called at each tag name when subparsing, must return
* true when the expected end tag is reached.
*
* @param \Twig_Token $token
* @return bool
*/
public function decideMyTagFork(\Twig_Token $token)
{
return $token->test(array ('endmytag'));
}
/**
* Your tag name: if the parsed tag match the one you put here, your parse()
* method will be called.
*
* @return string
*/
public function getTag()
{
return 'mytag';
}
}
编译器是用PHP编写标签应该做什么的代码。在您的示例中,您希望调用一个函数,其中body作为第一个参数,所有标记参数作为其他参数。
由于在{% mytag %}
和{% endmytag %}
之间输入的正文可能很复杂并且还编译了自己的代码,我们应该使用输出缓冲(ob_start()
/ ob_get_clean()
)来填充functionToCall()
的论点。
MyTagNode.php
<?php
class MyTagNode extends \Twig_Node
{
public function __construct($params, $lineno = 0, $tag = null)
{
parent::__construct(array ('params' => $params), array (), $lineno, $tag);
}
public function compile(\Twig_Compiler $compiler)
{
$count = count($this->getNode('params'));
$compiler
->addDebugInfo($this);
for ($i = 0; ($i < $count); $i++)
{
// argument is not an expression (such as, a \Twig_Node_Textbody)
// we should trick with output buffering to get a valid argument to pass
// to the functionToCall() function.
if (!($this->getNode('params')->getNode($i) instanceof \Twig_Node_Expression))
{
$compiler
->write('ob_start();')
->raw(PHP_EOL);
$compiler
->subcompile($this->getNode('params')->getNode($i));
$compiler
->write('$_mytag[] = ob_get_clean();')
->raw(PHP_EOL);
}
else
{
$compiler
->write('$_mytag[] = ')
->subcompile($this->getNode('params')->getNode($i))
->raw(';')
->raw(PHP_EOL);
}
}
$compiler
->write('call_user_func_array(')
->string('functionToCall')
->raw(', $_mytag);')
->raw(PHP_EOL);
$compiler
->write('unset($_mytag);')
->raw(PHP_EOL);
}
}
创建扩展程序以展示您的TokenParser更清洁,因为如果您的扩展程序需要更多,您将在此声明所有必需的内容。
MyTagExtension.php
<?php
class MyTagExtension extends \Twig_Extension
{
public function getTokenParsers()
{
return array (
new MyTagTokenParser(),
);
}
public function getName()
{
return 'mytag';
}
}
mytag.php
<?php
require_once(__DIR__ . '/Twig-1.15.1/lib/Twig/Autoloader.php');
Twig_Autoloader::register();
require_once("MyTagExtension.php");
require_once("MyTagTokenParser.php");
require_once("MyTagNode.php");
$loader = new Twig_Loader_Filesystem(__DIR__);
$twig = new Twig_Environment($loader, array (
// if you want to look at the generated code, uncomment this line
// and create the ./generated directory
// 'cache' => __DIR__ . '/generated',
));
function functionToCall()
{
$params = func_get_args();
$body = array_shift($params);
echo "body = {$body}", PHP_EOL;
echo "params = ", implode(', ', $params), PHP_EOL;
}
$twig->addExtension(new MyTagExtension());
echo $twig->render("mytag.twig", array('firstname' => 'alain'));
mytag.twig
{% mytag 1 "test" (2+3) firstname %}Hello, world!{% endmytag %}
结果
body = Hello, world!
params = 1, test, 5, alain
如果启用缓存,则可以看到生成的结果:
protected function doDisplay(array $context, array $blocks = array())
{
// line 1
ob_start();
echo "Hello, world!";
$_mytag[] = ob_get_clean();
$_mytag[] = 1;
$_mytag[] = "test";
$_mytag[] = (2 + 3);
$_mytag[] = (isset($context["firstname"]) ? $context["firstname"] : null);
call_user_func_array("functionToCall", $_mytag);
unset($_mytag);
}
对于这种特定情况,即使您将其他{% mytag %}
放入{% mytag %}
(例如{% mytag %}Hello, world!{% mytag %}foo bar{% endmytag %}{% endmytag %}
),这也会有效。但是,如果您正在构建此类标记,则可能会使用更复杂的代码,并且即使您在解析树中更深入,也会覆盖$_mytag
变量,因为它具有相同的名称。
所以让我们通过让它变得健壮来完成这个样本。
NodeVisitor
就像一个监听器:当编译器读取解析树以生成代码时,它将在进入或离开节点时输入所有已注册的NodeVisitor
。
所以我们的目标很简单:当我们输入MyTagNode
类型的节点时,我们会增加一个深度计数器,当我们离开节点时,我们会减少这个计数器。在编译器中,我们将能够使用此计数器生成要使用的正确变量名称。
MyTagNodeVisitor.php
<?php
class MyTagNodevisitor implements \Twig_NodeVisitorInterface
{
private $counter = 0;
public function enterNode(\Twig_NodeInterface $node, \Twig_Environment $env)
{
if ($node instanceof MyTagNode)
{
$node->setAttribute('counter', $this->counter++);
}
return $node;
}
public function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env)
{
if ($node instanceof MyTagNode)
{
$node->setAttribute('counter', $this->counter--);
}
return $node;
}
public function getPriority()
{
return 0;
}
}
然后在您的分机中注册NodeVisitor:
MyTagExtension.php
class MyTagExtension
{
// ...
public function getNodeVisitors()
{
return array (
new MyTagNodeVisitor(),
);
}
}
在编译器中,将所有"$_mytag"
替换为sprintf("$mytag[%d]", $this->getAttribute('counter'))
。
MyTagNode.php
// ...
// replace the compile() method by this one:
public function compile(\Twig_Compiler $compiler)
{
$count = count($this->getNode('params'));
$compiler
->addDebugInfo($this);
for ($i = 0; ($i < $count); $i++)
{
// argument is not an expression (such as, a \Twig_Node_Textbody)
// we should trick with output buffering to get a valid argument to pass
// to the functionToCall() function.
if (!($this->getNode('params')->getNode($i) instanceof \Twig_Node_Expression))
{
$compiler
->write('ob_start();')
->raw(PHP_EOL);
$compiler
->subcompile($this->getNode('params')->getNode($i));
$compiler
->write(sprintf('$_mytag[%d][] = ob_get_clean();', $this->getAttribute('counter')))
->raw(PHP_EOL);
}
else
{
$compiler
->write(sprintf('$_mytag[%d][] = ', $this->getAttribute('counter')))
->subcompile($this->getNode('params')->getNode($i))
->raw(';')
->raw(PHP_EOL);
}
}
$compiler
->write('call_user_func_array(')
->string('functionToCall')
->raw(sprintf(', $_mytag[%d]);', $this->getAttribute('counter')))
->raw(PHP_EOL);
$compiler
->write(sprintf('unset($_mytag[%d]);', $this->getAttribute('counter')))
->raw(PHP_EOL);
}
不要忘记在示例中包含NodeVisitor:
mytag.php
// ...
require_once("MyTagNodeVisitor.php");
自定义标签是扩展树枝的一种非常强大的方式,这个介绍为您提供了一个良好的开端。这里没有描述很多功能,但是通过查看twig内置扩展,由我们编写的类扩展的抽象类,以及通过读取由twig文件生成的生成的PHP代码,您将获得所有内容创建你想要的任何标签。
答案 1 :(得分:1)
查看文档之后..不确定它是否遵循所有标准,但它有效..
require 'Twig/Autoloader.php';
Twig_AutoLoader::register();
class MyTag_TokenParser extends Twig_TokenParser
{
public function parse(Twig_Token $token)
{
$parser = $this->parser;
$stream = $parser->getStream();
if (!$stream->test(Twig_Token::BLOCK_END_TYPE))
$values = $this->parser->getExpressionParser()
->parseMultitargetExpression();
$stream->expect(Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideMyTagEnd'), true);
$stream->expect(Twig_Token::BLOCK_END_TYPE);
return new MyTag_Node($body, $values, $token->getLine(), $this->getTag());
}
public function decideMyTagEnd(Twig_Token $token)
{
return $token->test('endmytag');
}
public function getTag()
{
return 'mytag';
}
}
class MyTag_Node extends Twig_Node
{
public function __construct(Twig_NodeInterface $body, $values,
$line, $tag = null)
{
if ($values)
parent::__construct(array('body' => $body, 'values' => $values),
array(), $line, $tag);
else
parent::__construct(array('body' => $body), array(), $line, $tag);
}
public function compile(Twig_Compiler $compiler)
{
$compiler
->addDebugInfo($this)
->write("ob_start();\n")
->subcompile($this->getNode('body'))
->write("my_func(ob_get_clean()");
if ($this->hasNode('values'))
foreach ($this->getNode('values') as $node) {
$compiler->raw(", ")
->subcompile($node);
};
$compiler->raw(");\n");
}
}
function my_func()
{
$args = func_get_args();
print_r($args);
}
$loader = new Twig_Loader_String();
$twig = new Twig_Environment($loader);
$twig->addTokenParser(new MyTag_TokenParser());
$template =<<<TEMPLATE
{% mytag %}
test1
{% endmytag %}
{% mytag 'var1' %}
test2
{% endmytag %}
TEMPLATE;
echo $twig->render($template);