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

Я пытаюсь создать собственный тег Twig, например:

{% mytag 'foo','bar' %}
   Hello world!!
{% endmytag %}

Этот тег должен печатать выходные данные my func("Hello world!!", "foo", "bar").

Может ли кто-нибудь опубликовать некоторый пример кода для создания такого пользовательского тега? Тот, кто может принять произвольное количество параметров, мне еще более ценится.

note: мне не интересно создавать пользовательскую функцию, мне нужно, чтобы тело тега передавалось как первый параметр.

Ответы

Ответ 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: текст, который не принимает участие в языке ветки: например, в коде Twig Hello, {{ 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, вы получите все, чтобы создать любой тег, который вы хотите.

Загрузить этот образец

Ответ 2

Посмотрев документацию.. Не уверен, что она соответствует всем стандартам, но она работает.

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);