当前位置: 首页 > 面试题库 >

如何创建执行回调的树枝自定义标签?

徐皓君
2023-03-14
问题内容

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

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

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

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

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


问题答案:

在谈论标签之前,您应该了解Twig在内部如何工作。

  • 首先,由于Twig代码可以放在文件,字符串甚至数据库中,因此Twig打开并使用Loader读取流。最著名的加载器是Twig_Loader_Filesystem从文件中打开树枝代码,并Twig_Loader_Array直接从字符串中获取树枝代码。

  • 然后,对该树枝代码进行解析以构建一个分析树,其中包含该树枝代码的对象表示。每个对象都称为Node,因为它们是树的一部分。至于其他语言,嫩枝制成的令牌,如{%{#function()"string"…等等枝杈语言结构会读几令牌来建立正确的节点。

  • 然后遍历分析树,并将其编译为PHP代码。生成的PHP类遵循该Twig_Template接口,因此渲染器可以调用doDisplay该类的方法以生成最终结果。

如果启用缓存,则可以查看那些生成的文件并了解发生了什么。

让我们开始平稳练习…

所有内部树枝标签,如{% block %}{% set %}…都使用相同的接口,自定义标签开发的,所以如果你需要一些特定的样品,你可以看看枝杈源代码。

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

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:“开始执行语句”令牌, {%

  • 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起),插值是Twig字符串内部的表达式解释,例如"my string, my #{variable} and 1+1 = #{1+1}"。插值的开始是#{

  • INTERPOLATION_END_TYPE,即“结束插值”令牌(自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 %}

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

下载此样本



 类似资料:
  • 问题内容: 如何创建自定义javadoc标记,例如@pre / @post?我找到了一些解释它的链接,但是我还没有运气。这些是一些链接: http://www.developer.com/java/other/article.php/3085991/Javadoc- Programming.html http://java.sun.com/j2se/1.5.0/docs/tooldocs/wind

  • 问题内容: 我想学习如何为html创建具有特殊属性和行为的自定义标签,如果有人可以提供建议,我将不胜感激。 问题答案: HTML5Rocks.com上有一篇有趣且深入的文章,介绍如何使用自定义元素:自定义元素:在HTML中定义新元素 这是该文章的摘录。 实例化元素 创建元素的常用技术仍然适用于自定义元素。与任何标准元素一样,它们可以用HTML声明或使用JavaScript在DOM中创建。实例化自定

  • 问题内容: 我需要做的就是在当前函数执行结束时执行一个回调函数。 此功能的使用者应如下所示: 我该如何实施? 问题答案: 实际上,您的代码将按原样工作,只需将回调声明为参数即可,您可以使用参数名称直接调用它。 基础知识 那会叫,这会叫,这会提醒“东西在这里”。 请注意,传递函数 引用 ()而不是调用函数并传递其结果()非常重要。在您的问题中,您可以正确执行此操作,但是值得指出,因为这是一个常见错误

  • 基本上,我想知道我是否可以创建一个树并在JavaFX上自定义它...我试着去做,但到目前为止还不能用这个代码做任何事情... 我在质疑自己,这是否是正确的“技术”,可以解决我想做的事情... 我从https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm#babjgggf看到了这个教程,但我对这个教程真的很困惑...我不太了解细胞工厂的机制

  • 默认情况下,Navicat Monitor 从受监控的实例收集一组预设的服务器指标。你可能想要添加自己的查询,以收集特定实例的一些自定义性能指标,并在指标值超过某些阈值和持续时间时接收有关自定义数据的警报。若要配置自定义指标,请前往“配置”->“自定义指标”。 创建自定义指标和警报 在自定义指标页面中,点击“+ 新建自定义指标”。 【步骤一】输入自定义指标的定义: 指标名 输入自定义指标的名。 描

  • 问题内容: 我正在尝试在javaFX中创建自定义光标。这是我的代码: Windows 8.1的游标创建无效吗? 问题答案: 检出ImageCursor.getBestSize()方法和ImageCursor.getMaximumColors()并查看它们返回的内容,然后尝试匹配最佳大小和最大颜色的自定义光标图像。对于Windows 8.1,这很可能是32x32的光标。 这是来自javadoc 的引