如何创建执行回调的树枝自定义标签? [英] How to create a twig custom tag that executes a callback?

查看:79
本文介绍了如何创建执行回调的树枝自定义标签?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

任何人都可以发布一些示例代码来创建这种自定义标签吗?一个可以接受任意数量参数的代码会让我更加感激.

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

解决方案

理论

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

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

  • 然后,解析此树枝代码以构建一个分析树,其中包含该树枝代码的对象表示形式.每个对象都称为Node,因为它们是树的一部分.与其他语言一样,Twig由令牌组成,例如{%{#function()"string" ...,因此Twig语言构造将读取多个令牌以构建正确的节点.

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

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


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

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

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

TokenParser

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

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

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

作为参考,Twig令牌可以是以下内容:

  • EOF_TYPE:流的最后一个标记,指示结束.

  • TEXT_TYPE:不属于树枝语言的文本:例如,在树枝代码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),插值是细枝字符串内部的表达式解释,例如"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 %}

结果

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的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(),
        );
    }

}

在编译器中,将所有"$_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代码,您将获得创建任何内容的所有功能您想要的标签.

下载此示例

I'm trying to create a custom Twig tag such as this:

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

This tag should print the output of my func("Hello world!!", "foo", "bar").

Can anybody post some sample code for creating such custom tag? One that may accept a arbitrary number of parameters would me even more appreciated.

note: I'm not interested in creating a custom function, I need to have the body of the tag passed in as the first parameter.

解决方案

Theory

Before speaking about tags, you should understand how Twig works internally.

  • First, as Twig code can be put on a file, on a string or even on a database, Twig opens and reads your stream using a Loader. Most known loaders are Twig_Loader_Filesystem to open twig code from a file, and Twig_Loader_Array to get twig code directly from a string.

  • Then, this twig code is parsed to build a parse tree, containing an object representation of the twig code. Each object are called Node, because they are part of a tree. As other languages, Twig is made of tokens, such as {%, {#, function(), "string"... so Twig language constructs will read for several tokens to build the right node.

  • The parse tree is then walked across, and compiled into PHP code. The generated PHP classes follow the Twig_Template interface, so the renderer can call the doDisplay method of that class to generate the final result.

If you enable caching, you can see those generated files and understand what's going on.


Let's start practicing smoothly...

All internal twig tags, such as {% block %}, {% set %}... are developed using the same interfaces as custom tags, so if you need some specific samples, you can look at Twig source code.

But, the sample you want is a good start anyway, so let's develop it.

The TokenParser

The token parser's goal is to parse and validate your tag arguments. For example, the {% macro %} tag requires a name, and will crash if you give a string instead.

When Twig finds a tag, it will look into all registered TokenParser classes the tag name returned by getTag() method. If the name match, then Twig calls the parse() method of that class.

When parse() is called, the stream pointer is still on the tag name token. So we should get all inline arguments, and finish the tag declaration by finding an BLOCK_END_TYPE token. Then, we subparse the tag's body (what is contained inside the tag, as it also may contain twig logic, such as tags and other stuffs): the decideMyTagFork method will be called each time a new tag is found in the body: and will break the sub parsing if it returns true. Note that this method name does not take part of the interface, that's just a standard used on Twig's built-in extensions.

For reference, Twig tokens can be the following:

  • EOF_TYPE: last token of the stream, indicating the end.

  • TEXT_TYPE: the text that does not take part of twig language: for example, in the Twig code Hello, {{ var }}, hello, is a TEXT_TYPE token.

  • BLOCK_START_TYPE: the "begin to execute statement" token, {%

  • VAR_START_TYPE: the "begin to get expression result" token, {{

  • BLOCK_END_TYPE: the "finish to execute statement" token, %}

  • VAR_END_TYPE: the "finish to get expression result" token, }}

  • NAME_TYPE: this token is like a string without quotes, just like a variable name in twig, {{ i_am_a_name_type }}

  • NUMBER_TYPE: nodes of this type contains number, such as 3, -2, 4.5...

  • STRING_TYPE: contains a string encapsulated with quotes or doublequotes, such as 'foo' and "bar"

  • OPERATOR_TYPE: contains an operator, such as +, -, but also ~, ?... You will about never need this token as Twig already provide an expression parser.

  • INTERPOLATION_START_TYPE, the "begin interpolation" token (since Twig >= 1.5), interpolations are expressions interpretation inside twig strings, such as "my string, my #{variable} and 1+1 = #{1+1}". Beginning of the interpolation is #{.

  • INTERPOLATION_END_TYPE, the "end interpolation" token (since Twig >= 1.5), unescaped } inside a string when an interpolation was open for instance.

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';
   }

}

The compiler

The compiler is the code that will write in PHP what your tag should do. In your example, you want to call a function with body as first parameter, and all tag arguments as other parameters.

As the body entered between {% mytag %} and {% endmytag %} might be complex and also compile its own code, we should trick using output buffering (ob_start() / ob_get_clean()) to fill the functionToCall()'s argument.

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

}

The extension

That's cleaner to create an extension to expose your TokenParser, because if your extension needs more, you'll declare everything's required here.

MyTagExtension.php

<?php

class MyTagExtension extends \Twig_Extension
{

   public function getTokenParsers()
   {
      return array (
              new MyTagTokenParser(),
      );
   }

   public function getName()
   {
      return 'mytag';
   }

}

Let's test it!

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

Going further

If you enable your cache, you can see the generated result:

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

For this specific case, this will work even if you put others {% mytag %} inside a {% mytag %} (eg, {% mytag %}Hello, world!{% mytag %}foo bar{% endmytag %}{% endmytag %}). But if you're building such a tag, you will probably use more complex code, and overwrite your $_mytag variable by the fact it has the same name even if you're deeper in the parse tree.

So let's finish this sample by making it robust.

The NodeVisitor

A NodeVisitor is like a listener: when the compiler will read the parse tree to generate code, it will enter all registered NodeVisitor when entering or leaving a node.

So our goal is simple: when we enter a Node of type MyTagNode, we'll increment a deep counter, and when we leave a Node, we'll decrement this counter. In the compiler, we will be able to use this counter to generate the right variable name to use.

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

}

Then register the NodeVisitor in your extension:

MyTagExtension.php

class MyTagExtension
{

    // ...
    public function getNodeVisitors()
    {
        return array (
                new MyTagNodeVisitor(),
        );
    }

}

In the compiler, replace all "$_mytag" by 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);
   }

Don't forget to include the NodeVisitor inside the sample:

mytag.php

// ...
require_once("MyTagNodeVisitor.php");

Conclusion

Custom tags are a very powerful way to extend twig, and this introduction gives you a good start. There are lots of features not described here, but by looking close to twig built-in extensions, abstract classes extended by the classes we written, and moreover by reading the generated php code resulting from twig files, you'll get everything to create any tag you want.

Download this sample

这篇关于如何创建执行回调的树枝自定义标签?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆