Как создать собственный твидовый тэг, который выполняет обратный вызов?

Я пытаюсь создать собственный тег 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.

Но образец, который вы хотите, является хорошим началом в любом случае, поэтому давайте его разработаем.

TokenParser

Цель анализатора токенов состоит в анализе и проверке ваших аргументов тега. Например, для тега {% 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 подобен слушателю: когда компилятор будет считывать дерево разбора для генерации кода, он будет вводить все зарегистрированные 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);