我正在尝试创建一个像这样的自定义Twig标签:

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

此标记应打印my func("Hello world!!", "foo", "bar")的输出。

有人可以发布一些示例代码来创建此类自定义标签吗? 可以接受任意数量的参数的我会更加感激。

注意:我对创建自定义函数不感兴趣,我需要将标记的主体作为第一个参数传入。

最佳答案

理论

在谈论标签之前,您应该了解Twig在内部的工作方式。

  • 首先,由于Twig代码可以放在文件,字符串甚至数据库中,因此Twig打开并使用Loader读取流。最知名的加载器是Twig_Loader_Filesystem(可从文件中打开 Twig 代码),以及Twig_Loader_Array(可从字符串中直接获取 Twig 代码)。
  • 然后,解析此 Twig 代码以构建一个分析树,其中包含该 Twig 代码的对象表示形式。每个对象称为Node,因为它们是树的一部分。与其他语言一样,Twig由 token 组成,例如{%{#function()"string" ...,因此Twig语言构造将读取多个 token 以构建正确的节点。
  • 然后遍历分析树,并将其编译为PHP代码。生成的PHP类遵循Twig_Template接口(interface),因此渲染器可以调用该类的doDisplay方法来生成最终结果。

  • 如果启用缓存,则可以查看这些生成的文件并了解正在发生的情况。

    让我们开始平稳练习...

    所有内部的Twig标签,例如{% block %}{% set %} ...都是使用与自定义标签相同的接口(interface)开发的,因此,如果需要一些特定的示例,可以查看Twig源代码。

    但是,无论如何,您想要的示例都是一个好的开始,所以让我们进行开发。

    TokenParser

    token 解析器的目标是解析和验证标签参数。例如,{% macro %}标记需要一个名称,而如果您输入一个字符串,则会崩溃。

    当Twig找到标签时,它将查找TokenParser方法返回的所有标签名称getTag()。如果名称匹配,则Twig调用该类的parse()方法。

    调用parse()时,流指针仍位于标记名称标记上。因此,我们应该获取所有内联参数,并通过找到BLOCK_END_TYPE token 来完成标签声明。然后,我们将标签的主体分割(标签内部包含什么,因为它还可能包含 Twig 逻辑,例如标签和其他内容):每次在主体中找到新标签时,就会调用decideMyTagFork方法:如果子解析返回true,则中断该子解析。请注意,此方法名称不属于接口(interface),这只是Twig内置扩展中使用的标准。

    作为引用,Twig token 可以是以下几种:
  • EOF_TYPE:流的最后一个标记,指示结束。
  • TEXT_TYPE:不属于 Twig 语言的文本:例如,在 Twig 代码Hello, {{ var }}中,hello,TEXT_TYPE token 。
  • BLOCK_START_TYPE:“开始执行语句” token ,{%
  • VAR_START_TYPE:“开始获取表达结果” token ,{{
  • BLOCK_END_TYPE:“完成执行语句” token ,%}
  • VAR_END_TYPE:“完成以获取表达结果” token ,}}
  • NAME_TYPE:该 token 就像没有引号的字符串,就像 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起),插值是 Twig 字符串内部的表达式解释,例如"my string, my #{variable} and 1+1 = #{1+1}"。插值的开始是#{
  • INTERPOLATION_END_TYPE,“结束插值” token (自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作为第一个参数,而所有tag参数作为其他参数。

    由于在{% 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
    NodeVisitor类似于监听器:当编译器将读取解析树以生成代码时,它将在进入或离开节点时输入所有已注册的MyTagNode

    所以我们的目标很简单:当我们输入"$_mytag"类型的Node时,我们将增加一个深计数器,而当我们离开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(),
            );
        }
    
    }
    

    在编译器中,将所有sprintf("$mytag[%d]", $this->getAttribute('counter'))替换为ojit_code。

    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内置扩展,由我们编写的类扩展的抽象类,以及通过阅读由twig文件生成的php代码,您将获得创建任何内容的一切标记你想要的。

    Download this sample

    关于php - 如何创建执行回调的 Twig 自定义标签?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/26170727/

    10-15 16:54