Я пытаюсь создать собственный тег Twig, например:
{% mytag 'foo','bar' %} Hello world!! {% endmytag %}
Этот тег должен печатать выходные данные my func("Hello world!!", "foo", "bar")
.
Может ли кто-нибудь отправить образец кода для создания такого настраиваемого тега? Тот, который может принять произвольное количество параметров, мне еще более ценится.
note : Мне не интересно создавать пользовательскую функцию, мне нужно, чтобы тело тега передавалось как первый параметр.
Прежде чем говорить о тегах, вы должны понять, как Twig работает внутри.
Во-первых, поскольку код Twig может быть помещен в файл, в строку или даже в базу данных, Twig открывает и считывает ваш поток с помощью Loader. Наиболее известными загрузчиками являются Twig_Loader_Filesystem
чтобы открыть Twig_Loader_Filesystem
код из файла, и Twig_Loader_Array
чтобы получить код twig непосредственно из строки.
Затем этот код 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
): метод decideMyTagFork
будет вызываться каждый раз, когда новый тег находится в теле: и будет разбейте подэлемент, если он вернет true. Обратите внимание, что это имя метода не входит в интерфейс, это стандарт, используемый для встроенных расширений Twig.
Для справки, токены Twig могут быть следующими:
EOF_TYPE
: последний токен потока, указывающий конец.
TEXT_TYPE
: текст, который не принимает участие в языке TEXT_TYPE
: например, в коде Twig Hello, {{ var }}
, hello,
является токеном TEXT_TYPE
.
BLOCK_START_TYPE
: BLOCK_START_TYPE
«begin to execute», {%
VAR_START_TYPE
: VAR_START_TYPE
«начать получать результат выражения», {{
BLOCK_END_TYPE
: BLOCK_END_TYPE
«завершение выполнения», %}
VAR_END_TYPE
: VAR_END_TYPE
«закончить, чтобы получить результат выражения», }}
NAME_TYPE
: этот токен похож на строку без кавычек, точно так же как имя переменной в twig, {{ 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
, токен «конечной интерполяции» (с 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()
), чтобы заполнить аргумент ob_get_clean()
,
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); } }
с<?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); }
с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
при входе или выходе из узла.
Итак, наша цель проста: когда мы вводим Node типа MyTagNode
, мы 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 встроенные расширения, абстрактные классы, расширенные классами, которые мы написали, и, кроме того, прочитав сгенерированный PHP-код, полученный из файлов twig, вы получите все, чтобы создать любой тег, который вы хотите.
Посмотрев документацию .. Не уверен, что она соответствует всем стандартам, но она работает ..
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);
от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);