Saturday 31 July 2010

Using the PHP Smarty Template Engine with the Zend Framework

This post is about how to use the Smarty Templating Engine together with the Zend Framework. I'm not going to discuss why you'd want to do this; I'm only going to show how easy this is to do.

There are a number of articles already on the web showing how to use Smarty and Zend Framework (for example, this one by Andrea Belvedere and this one on Zend's devzone) but they usually have two problems

  1. They are usually quite old and out of date.
  2. They don't cover using both Zend_View and Zend_Layout.
In this article, I've going to correct these two points. This article uses Zend Framework 1.10 and shows how to use Smarty for both the view and the layout.

Setting up


I'm going to assume that you're already comfortable setting up a new Zend Framework project using the Zend Tool, zf. If you're not, here is a good video showing how to do this in Netbeans 6.9. I'm also going to assume that you already have a working PHP development environment.

First thing is to download the Smarty library and copy it into the library directory in your project. When I did this, I renamed the Smarty directory to remove the version number, so that the Smarty code is actually in 'library/Smarty/libs/'.

For Smarty to work, it needs two additional directories, 'cache' and 'templates_c', on the top level of your project (on the same level as the directories called application, library and so on). These directories need to be readable and writeable to by PHP.

Next, we need to put some configuration values into the application's config file. In Zend Framework 1.10, this is a file call application.ini in the 'application/config' directory. Paste the following under [production]
smarty.dir = APPLICATION_PATH "/../library/Smarty/libs/"
smarty.template_dir = APPLICATION_PATH "/views/scripts/"
smarty.compile_dir = APPLICATION_PATH "/../templates_c"
smarty.config_dir = APPLICATION_PATH "/configs"
smarty.cache_dir = APPLICATION_PATH "/../cache"
smarty.caching = 0
smarty.compile_check = true 
These lines will be used to tell the Smarty engine where to find things. In the .ini file, APPLICATION_PATH refers to the directory called application, where your controllers, views and models reside, so 'APPLICATION_PATH "/../templates_c"' means start at that directory, go up one level, and then find a directory called 'templates_c'.

Bootstrap and Zend_View


In older versions of the Zend Framework, it was normal to put bootstrapping code into the index.php file in the public directory but this is no longer the recommended approach. In Zend Framework 1.10, your bootstrapping code goes into the class Bootstrap in the application directory. We need to override the _initView() method in the otherwise empty Bootstrap class.
protected function _initView() {
 require_once 'Smarty_View.php';
 $view = new Smarty_View($this->getOption('smarty'));
 $viewRender = Zend_Controller_Action_HelperBroker::getStaticHelper(
  'ViewRenderer'
 );
 $viewRender->setView($view);
 $viewRender->setViewSuffix('phtml');
 Zend_Controller_Action_HelperBroker::addHelper($viewRender);
 return $view;
}
This method simply changes the class of object used to represent the application's view. Normally, it would be Zend_View, but we're saying use something called Smarty_View instead. The values that we've already put into the application.ini file are put into Smarty_View's constructor.

Smarty_View extends Zend_View_Abstract; there are many versions of this on the web (including in the official Zend Framework documentation) but this version is based on Andrea Belvedere's version.
<?php

class Smarty_View extends Zend_View_Abstract {

  private $_smarty;

  public function __construct($data) {
    parent::__construct($data);
    require_once $data['dir'] . "Smarty.class.php";

    $this->_smarty = new Smarty();
    $this->_smarty->template_dir = $data['template_dir'];
    $this->_smarty->compile_dir = $data['compile_dir'];
    $this->_smarty->config_dir = $data['config_dir'];
    $this->_smarty->cache_dir = $data['cache_dir'];
    $this->_smarty->caching = $data['caching'];
    $this->_smarty->compile_check = $data['compile_check'];
  }

  public function getEngine() {
    return $this->_smarty;
  }

  public function __set($key, $val) {
    $this->_smarty->assign($key, $val);
  }

  public function __get($key) {
    return $this->_smarty->get_template_vars($key);
  }

  public function __isset($key) {
    return $this->_smarty->get_template_vars($key) != null;
  }

  public function __unset($key) {
    $this->_smarty->clear_assign($key);
  }

  public function assign($spec, $value=null) {
    if (is_array($spec)) {
      $this->_smarty->assign($spec);
      return;
    }
    $this->_smarty->assign($spec, $value);
  }

  public function clearVars() {
    $this->_smarty->clear_all_assign();
  }

  public function render($name) {
    return $this->_smarty->fetch(strtolower($name));
  }

  public function _run() {
    
  }

}
This class simply maps the Zend_View_Abstract methods to the Smarty equivalents and sets up up the Smarty engine. We can test this set up with the following code. Create an action with the following code
$this->view->entries = array('moo', 'dave', 'fred', 'andy', 'jo');
$this->view->moo = 'Moo';
and a view template with the code
{$moo|strtoupper} says:

<ol>
 {foreach from=$entries item=entry}
 <li class="{cycle values="odd,even"}">{$entry} - {$entry|strlen}</li>
 {/foreach}
</ol>

Zend_Layout


The previous set up is all that is needed to start using Smarty with Zend_View.Unfortunately, if you're also using Zend_Layout, this will now be broken. If you're not using Zend_Layout, 'zf enable layout' is the Zend Tool command to switch this feature on.

There are two problems we need to overcome. Firstly, Smarty can't find the layout.phtml file. This is because we've told Smarty to look for template files in 'application/views/scripts/' but the layout template is in 'application/layouts/scripts/'. This is actually very easy to correct. A little documented feature of Smarty is that you can give it an array of directories to search, and it will search each in turn until it finds the correct file.

Add the following configuration to your application.ini file.
smarty.layout_dir = APPLICATION_PATH "/layouts/scripts"
Then change the following line in Smarty_View from
$this->_smarty->template_dir = $data['template_dir'];
to
$this->_smarty->template_dir = array($data['template_dir'], $data['layout_dir']);
The second issue to resolve is a small conflict between the way Zend_View and Smarty are implemented; They both expect to execute the view template inside their own scope. In vanilla layout template, the keyword $this would point to the Zend_View object, but now it must point to the Smarty engine. This means that we can't use the normal syntax to access the Zend_Layout object in the layout template.

The fix for this is, again, quite simple. We simply need to add the Zend_View and Zend_Layout objects to the Smarty object as template variables with the following two lines in the Smarty_View constructor.
$this->assign('_view', $this);
$this->assign('_layout', $this->layout());
Now, in the layout template, where we would normally use
<?php $this->layout()->content ?>
we instead use
{$_layout->content}
and where we might previously use
<?php $this->headLink() ?>
we use instead
{$_view->headLink()}

That's all there is to it. You now can use the Smarty template engine while still being able to use Zend_View, Zend_Layout and helpers.

Lastly, notice the line 'smarty.caching = 0' in the config file. This switches off Smarty's static caching system which saves a copy of the static HTML output into the cache directory; this is fine for a development environment but you might want to consider turning it on in production.

18 comments:

  1. Hi, thanks for post.

    How to use hybrid Smarty_View and Zend_View?

    How to change view in controller?

    ReplyDelete
  2. Hi Moo - great post! I had been fighting with this for quite a while. One thing I used Smarty 3.0rc3 and for this you have to use getTemplateVars($key) instead of get_template_vars($key) in the Smarty_View class

    ReplyDelete
  3. Thanks, your post helped me a lot!

    ReplyDelete
  4. Avec ZF1.9 et Smarty, je n’arrive juste pas à faire appel à des fonctions, même simples dans la vue {$this->headStyle()}
    par contre, j’arrive bien à afficher la valeur de mes variables telles que {$book|upper} ou à en créer de nouvelles directement dans ma vue.
    si qql voit pourquoi ?

    ReplyDelete
  5. Hi Extraneous, thanks for pointing this out. I guess you could use PHP's method_exists() function to overcome this difference.

    ReplyDelete
  6. Hi ZED, thanks for the comment. I thought about your question and I can't think of an easy way to do that (or a use case, really).

    ReplyDelete
  7. Hi Ariden, I'm sorry but my French isn't great. I think that {$this->headStyle()} should actually be {$_view->headStyle()}. I'll have to investigate {$book|upper} and get back to you but have you tried {$book|strtoupper}?

    ReplyDelete
  8. Hi Extraneous, I've taken a look at the Smarty 3.0RC3 code and actually, the old method names are accounted for; they're using the __call() magic method to redirect the old method names to the new ones, so the code should work without modification.

    ReplyDelete
  9. i keep getting this error. banging my head why:

    Warning: Smarty error: unable to read resource: "layout.phtml" in /var/www/zf-tutorial/library/My/Smarty/Smarty.class.php on line 1093

    ReplyDelete
  10. I figures what the problem was. the script the views folder of the controller so you need to add this to you .ini

    smarty.views_dir = APPLICATION_PATH "/views/scripts"

    then add to the smarty class:
    $this->_smarty->template_dir = array($data['template_dir'], $data['views_dir'], $data['layout_dir']);

    ReplyDelete
  11. Hi great work. It saves me Hours!

    I buiold a resource plugin for Zend_Application based on your work:

    class Clever_Application_Resource_Smarty extends Zend_Application_Resource_ResourceAbstract
    {

    public function init()
    {
    require_once 'Smarty/View.php';

    $view = new Smarty_View($this->_options);
    $viewRender = Zend_Controller_Action_HelperBroker::getStaticHelper('ViewRenderer');
    $viewRender->setView($view);
    $viewRender->setViewSuffix($this->_getOption('suffix', 'tpl'));
    Zend_Controller_Action_HelperBroker::addHelper($viewRender);
    return $view;
    }

    /**
    * returns an Option or its default
    *
    * @param string $name
    * @param mixed $default
    *
    * @return mixed
    */
    protected function _getOption($name, $default = null)
    {
    return (isset($this->_options[$name]))
    ? $this->_options[$name]
    : $default;
    }
    }

    ReplyDelete
  12. And put this to your config.ini


    pluginPaths.Clever_Application_Resource = "Clever/Application/Resource"
    resources.smarty.dir = APPLICATION_PATH "/../library/Smarty/"
    resources.smarty.template_dir = APPLICATION_PATH "/views/scripts/"
    resources.smarty.layout_dir = APPLICATION_PATH "/layouts/scripts"
    resources.smarty.compile_dir = APPLICATION_PATH "/../templates_c"
    resources.smarty.config_dir = APPLICATION_PATH "/configs"
    resources.smarty.cache_dir = APPLICATION_PATH "/../cache"
    resources.smarty.caching = 0
    resources.smarty.compile_check = true
    resources.smarty.suffix = tpl

    ReplyDelete
  13. Hi Moo,

    I have been using this method and it works fine until i turn on the caching=1 in smarty. Now, every page I go only outputs the first compiled version of layout.phtml so I see the same page layout everytime in any other pages.

    ReplyDelete
  14. Thanks for the post, it's been very helpful.

    One question, how do you use view helpers in Smarty which require an array as a parameter e.g. the url helper?

    ReplyDelete
  15. Why not: $this->assign('this', $this);

    ReplyDelete
  16. hi,,

    i am confused where to create this Smarty_view.php file.. in file system of zf

    ReplyDelete