The best Smarty + Zend View Helpers solution!

Tags:

Originally posted in my old blog at My Opera

Coming on again with the Smarty and Zend View related articles, let's this time take a look at how to get Zend's View Helpers running with Smarty.

Other examples for this that I have seen use a syntax like this: {helper helper=Url p='array(something)' p2=stuff}, which is kind of ugly and the array parsing is done with eval, and we know that Eval is Evil.

Wouldn't a more elegant solution let you use helpers just like you use Smarty plugins? In the style of just typing the name of the helper and simple parameters? Let's see how to make that happen!


The solutions in this post will be based on the SmartyView class, introduced in Smarty + Zend View, take three, so check it out if you feel it might help you understand this one.

Starting

So, we want to have a nice syntax for calling Zend's viewhelpers from Smarty templates. First, we have some issues we have to find solutions for:

  • Smarty does not “know” about View Helpers
  • View helpers may need arrays or associative arrays as parameters. Smarty has no support for doing this out-of-the-box

The first one is quite simple to solve: Since the Zend_View class knows about viewhelpers and can call them, the SmartyView class also does that. Since we need to create a new class based on Smarty anyway, we can add a method for saving the View instance to it so that the new Smarty class can also call view helpers.

The second one needs more work. Since Smarty's syntax for passing parameters to functions differs a lot from the typical syntax, we need to think of a nice Smarty-like way of passing arrays and associative arrays as parameters. We also need to dig in the Smarty compiler class which converts the Smarty templates into PHP code. Isn't it great that I did the digging for you and you can just read about it here? :up:

For the syntax, I decided on this:

{helperName param=foo arr=bar arr=baz assoc.one=test assoc.two=hello}

This would be the same as writing this in normal Zend_View templates:

$this->helperName('foo', array('bar','baz'), array('one' => 'test', 'two' => 'hello');

I think this is very Smarty-like and at least I like the fact that I can call the parameters whatever I want as I could use it to make it more obvious what the parameters will change. Consider the following:

{makeList source=$myData sortBy=name}
or something like
{makeList $myData name}

Extending Smarty

So, let's first extend the Smarty class to add our shiny new features!

I'm going to call the class SmartyAdvanced, since I'm bad at making up good catchy names. Feel free to suggest anything better ;)

So as we learnt above, we need to add some methods into the Smarty class, namely a method for saving a Zend_View instance into it and a method for calling View Helpers from the compiled templates.

Thus…

<?php
require_once &#39;smarty/libs/Smarty.class.php&#39;;

class SmartyAdvanced extends Smarty
{
  private $_zendView;
 
  public function __construct()
  {
    parent::__construct();
    $this->compiler_class = &#39;SmartyAdvanced_Compiler&#39;;
  }
 
  public function setZendView(Zend_View_Abstract $view)
  {
    $this->_zendView = $view;
  }
 
  public function callViewHelper($name,$args)
  {
    $helper = $this->_zendView->getHelper($name);
 
    return call_user_func_array(array($helper,$name),$args);
  }  
}
?>

So here's our brand new SmartyAdvanced class. The constructor calls the Smarty class' constructor and sets the compiler class name to SmartyAdvanced_Compiler… we'll look at it a bit later.

The other two functions serve as the view helper methods. setZendView should be pretty obvious, and callViewHelper simply gets the view helper from the Zend_View class and calls it with the parameters provided.

Extending Smarty_Compiler

As you know, Smarty templates are compiled into PHP code to speed up the execution. Since some of the things we do are tampering with very internal things in Smarty, like the syntax, we need to extend the compiler to do things our way and not their way.

So we need to modify the compiler to understand our custom syntax and to make it so that Smarty will try to call View Helpers if it can't parse a template function call with its own functions.

There's one thing I want to point out before the code: In the constructor, the Zend_View object is specified by just making a new Zend_View instance. This is bad, but necessary.
Unless we want to go in and modify Smarty's code, possibly breaking our code in the case of an update, we have to instantiate a view here. A better option would be to use a view factory, as shown in one of my previous posts.

<?php
require_once &#39;smarty/libs/Smarty_Compiler.class.php&#39;;

class SmartyAdvanced_Compiler extends Smarty_Compiler
{
  private $_zendView;
 
  public function __construct()
  {
    parent::__construct();
    $this->_zendView = new Zend_View();
  }
 
  function _compile_compiler_tag($tagCommand, $tagArgs, &$output)
  {
    //We first try to use Smarty&#39;s own functionality to parse the tag
    $found = parent::_compile_compiler_tag($tagCommand,$tagArgs,$output);
 
    if(!$found)
    {
      try
      {
        //Check if helper exists and create output
        $helper = $this->_zendView->getHelper($tagCommand);
 
        $helperArgs = array();
 
        if($tagArgs !== null)
        {
          //Start parsing our custom syntax
          $params = explode(&#39; &#39;,$tagArgs);
          foreach($params as $p)
          {
            //Split each key=value pair to vars
            list($key,$value) = explode(&#39;=&#39;,$p,2);
            $section = &#39;&#39;;

            //If there&#39;s a dot in the key, it means we
            //need to use associative arrays
            if(strpos(&#39;.&#39;,$key) != -1)
              list($key,$section) = explode(&#39;.&#39;,$key);

            //Use Smarty&#39;s own functions to parse the value
            //so that if there&#39;s a variable, it gets changed to
            //properly point at a template variable etc.
            $value = $this->_parse_var_props($value);
 
            //Put the value into the arg array
            if($section == &#39;&#39;)
            {
              if(array_key_exists($key,$helperArgs))
              {
                if(is_array($helperArgs&#91;$key&#93;))
                  $helperArgs&#91;$key&#93;&#91;&#93; = $value;
                else
                  $helperArgs&#91;$key&#93; = array($helperArgs&#91;$key&#93;,$value);

              }
              else
                $helperArgs&#91;$key&#93; = $value;
            }
            else
            {
              if(!is_array($helperArgs&#91;$key&#93;))
                $helperArgs&#91;$key&#93; = array();

              $helperArgs&#91;$key&#93;&#91;$section&#93; = $value;
            }
          }
        }
 
        //Save the code to put to the template in the output
        $output = "<?php echo \$this->callViewHelper(&#39;$tagCommand&#39;,array(".$this->_createParameterCode($helperArgs).")); ?>";
        $found = true;
      }
      catch(Zend_View_Exception $e)
      {
        //Exception means the helper was not found
        $found = false;
      }
    }
 
    return $found;
  }
 
  //This function creates the code for the helper params
  private function _createParameterCode($params)
  {
    $code = &#39;&#39;;

    $i = 1;
    $pCount = count($params);
    foreach($params as $p)
    {
      if(is_array($p))
        $code .= &#39;array(&#39;.$this->_createParameterCode($p).&#39;)&#39;;
      else
        $code .= $p;
 
      if($i != $pCount)
        $code .= &#39;,&#39;;

      $i++;
    }
 
    return $code;
  }
}
?>

Okay so that's a bit more than previously. With that, we're all done for Smarty mods!

Using the new classes

Using the new classes is pretty easy. We need to do a small change to the SmartyView class' constructor. Replace…

$this->_smarty = new Smarty();

with the following

if(isset($config&#91;&#39;smartyClass&#39;&#93;))
  $this->_smarty = new $config&#91;&#39;smartyClass&#39;&#93;;
else
  $this->_smarty = new Smarty();

Doing this will enable us to pass the Smarty class we want to use as a parameter.

Like this…

$view = new SmartyView(array(
        &#39;compileDir&#39; => &#39;./template_c&#39;,
        &#39;helperPath&#39; => &#39;./application/views/helpers&#39;,
        &#39;pluginDir&#39; => &#39;./plugins&#39;,
        &#39;smartyClass&#39; => &#39;SmartyAdvanced&#39;
        ));
 
$view->getEngine()->setZendView($view);

All that is needed is to give the new SmartyAdvanced class name as the smartyClass parameter for the SmartyView configuration. Then we need to pass the view instance to setZendView so that Smarty will be able to call helpers.

Conclusion

Despite Smarty's code being not very modification/extension-friendly, it's possible to add a lot of interesting features to it as you can see. With these extensions, using View Helpers with Smarty as the template engine is much nicer in my opinion.

The code for SmartyAdvanced and SmartyAdvanced_Compiler can be found here.