Ответ 1
Теория
Прежде чем говорить о тегах, вы должны понимать, как Twig работает внутри.
-
Во-первых, поскольку код Twig может быть помещен в файл, в строку или даже в базу данных, Twig открывает и считывает ваш поток с помощью Loader. Наиболее известные загрузчики
Twig_Loader_Filesystem
, чтобы открыть код твика из файла, иTwig_Loader_Array
, чтобы получить код twig непосредственно из строки. -
Затем этот синтаксический код обрабатывается для построения дерева синтаксического анализа, содержащего представление объекта кода ветки. Каждый объект называется
Node
, потому что они являются частью дерева. Как и другие языки, Twig состоит из токенов, таких как{%
,{#
,function()
,"string"
... поэтому конструкции языка Twig будут читать несколько токенов для создания правильного node. -
Затем дерево разбора перебирается и скомпилируется в PHP-код. Сгенерированные классы PHP следуют интерфейсу
Twig_Template
, поэтому рендерер может вызвать методdoDisplay
этого класса для генерации конечного результата.
Если вы включите кеширование, вы можете увидеть эти сгенерированные файлы и понять, что происходит.
Позвольте начать плавно плавно...
Все внутренние твиг-теги, такие как {% block %}
, {% set %}
..., разрабатываются с использованием тех же интерфейсов, что и пользовательские теги, поэтому, если вам нужны некоторые конкретные образцы, вы можете посмотреть исходный код Twig.
Но образец, который вы хотите, является хорошим началом в любом случае, поэтому давайте развить его.
TokenParser
Цель анализатора токенов заключается в анализе и проверке ваших аргументов тега. Например, тегу {% macro %}
требуется имя, и он будет сбой, если вместо этого вы укажете строку.
Когда Twig находит тег, он будет рассматривать все зарегистрированные классы TokenParser
имя тега, возвращаемое методом getTag()
. Если имя совпадает, то Twig вызывает метод parse()
этого класса.
Когда вызывается parse()
, указатель потока по-прежнему находится в токене имени тега. Поэтому мы должны получить все встроенные аргументы и закончить объявление тега, найдя токен BLOCK_END_TYPE
. Затем мы подбираем тело тега (то, что содержится внутри тега, так как оно также может содержать логику ветки, например, теги и другие вещи): метод decideMyTagFork
будет вызываться каждый раз, когда новый тег находится в теле: и будет нарушать подэлемент, если он вернет true. Обратите внимание, что это имя метода не входит в интерфейс, это просто стандарт, используемый для встроенных расширений Twig.
Для справки, токены Twig могут быть следующими:
-
EOF_TYPE
: последний токен потока, указывающий конец. -
TEXT_TYPE
: текст, который не принимает участие в языке ветки: например, в коде TwigHello, {{ var }}
,hello,
- токенTEXT_TYPE
. -
BLOCK_START_TYPE
: токен "begin to execute",{%
-
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 interolation" (начиная с Twig >= 1.5), unescaped}
внутри строки, когда интерполяция была открыта, например.
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, что должен делать ваш тег. В вашем примере вы хотите вызвать функцию с телом в качестве первого параметра и все аргументы тега в качестве других параметров.
Поскольку тело, введенное между {% 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 %}
Result
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
A NodeVisitor
похож на прослушиватель: когда компилятор будет считывать дерево разбора для генерации кода, он будет вводить все зарегистрированные NodeVisitor
при вводе или выходе из node.
Итак, наша цель проста: когда мы вводим Node типа MyTagNode
, мы увеличим счетчик глубины, и когда мы оставим Node, мы уменьшим этот счетчик. В компиляторе мы сможем использовать этот счетчик для создания правильного имени переменной для использования.
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 встроенные расширения, абстрактные классы, расширенные классами, которые мы написали, и, кроме того, прочитав сгенерированный PHP-код, полученный из файлов twig, вы получите все, чтобы создать любой тег, который вы хотите.