The best Smarty + Zend View Helpers solution!

November 3, 2007 – 11:58 am 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.

Share this:
  1. 10 Responses to “The best Smarty + Zend View Helpers solution!”

  2. May I suggest that you would create, along with the other relevant classes, a sub-directory in your own framework extensions. I have my implementation this changed the name from SmartyAdvanced and SmartyAdvanced_Compiler to ITPro_View_Smarty_Core and ITPro_View_Smarty_Compiler respectively, and combined your ViewFactory and SmartyView classes into ITPro_View_Smarty. I have found this means less need to subclass Zend_Loader to get them to work nicely, and following the naming convention of Zend means less to document. I also disliked having to hand a view object back to itself so had that as part of the factory.

    May I also suggest that you tweak this and the other code to fully comply with the Zend Coding guidelines, so that others using your code can drop it in verbatim. The tweaks were minor however, and I can send you a copy with every brace and space per the Zend manual if you would like.

    Aside from these comments, works like a charm.

    By Barney Hanlon on Nov 21, 2007

  3. Barney, you’re right about the naming stuff, but I’m just sliiightly lazy there ;)
    I do try, though.

    The reason you have to pass the view to the SmartyAdvanced class is because I couldn’t really think of any better way. Sure, doing new Zend_View or something in it would work, but that wouldn’t inherit your view configurations, if any.

    The view factory could do it, yeah, but as I inteded it as more “general purprose”, I didn’t want to put any SmartyAdvanced-specific code in it.

    I’m considering creating a public SVN repository for my classes, with good Zend-style names. That should make it a simple job to grab the files etc.

    By Jani Hartikainen on Nov 21, 2007

  4. Hey Jani,

    if you decide to do the SVN repos that would be excellent. Also, may I offer to add some examples of implementations on a protected branch, perhaps together we can up with some very interesting work.

    By Barney Hanlon on Nov 22, 2007

  5. Svn repository UP!

    By Jani Hartikainen on Nov 23, 2007

  6. Would it be possible to handle helper code like :
    $this->headMeta()->appendName(‘keywords’, ‘framework php productivity’);

    I don’t see how this can be done ?

    By Wim Godden on Feb 9, 2008

  7. Wim,

    You’re right, it can’t be done. Smarty doesn’t really support doing foo->bar->baz style syntax to begin with, but it could be hacked in.

    By Jani Hartikainen on Feb 19, 2008

  8. I can’t seem to get the ‘assoc.key=value’ to work.. any ideas?

    Cheers M

    By Matt on Sep 1, 2008

  9. to make assoc.key=value work i need to modify the _createParameterCode function.

    private function _createParameterCode($params, $lastWasArray = false)
    {
    $code = ”;

    $i = 1;
    $pCount = count($params);
    foreach($params as $index => $p)
    {
    if (is_array($p)) {
    $code .= ‘array(‘.$this->_createParameterCode($p, true).’)';
    } else {
    if ($lastWasArray) {
    $code .= $index.’ => ‘.$p;
    } else {
    $code .= $p;
    }
    }
    if($i != $pCount)
    $code .= ‘,';

    $i++;
    }
    return $code;
    }

    For me it worked

    regards ;)

    By Thesee on Oct 25, 2008

  10. function _createParameterCode($params)
    {
    $code = ”;

    $i = 1;
    $pCount = count($params);
    foreach ($params as $key => $p)
    {
    if (is_array($p))
    $code .= ‘array(‘ . $this->_createParameterCode($p) . ‘)';
    else
    $code .= $key.’=>’.$p;

    if ($i != $pCount)
    $code .= ‘,';

    $i++;
    }

    return $code;
    }

    By zbw911 on Mar 23, 2010

  1. 1 Trackback(s)

  2. Nov 8, 2007: Summing up Smarty and Zend View | CodeUtopia

Post a Comment

You can use some HTML (a, em, strong, etc.). If you want to post code, use <pre lang="PHP">code here</pre> (you can replace PHP with the language you are posting)