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.

Saturday 24 July 2010

Using jQuery and JSONP to load Twitter statuses

In this post I wanted to quickly share how to use jQuery to use Twitter's API to load tweets into a HTML page. This uses AJAX but importantly, it doesn't use any server side code like PHP to load the tweets. All the processing is done on the client's browser which communicates directly with the twitter server. This is possible because Twitter's API supports JSONP, an extension to JSON.

Wikipedia has a good discussion of JSONP but basically, it allows a page to request data from a server other than the one that originally served the page; this isn't usually allowed because of the 'same origin policy' that states that a dynamic element within a browser, such as JavaScript or Flash, can only communicate back to the server that it was loaded from. JSONP gets around this by injecting the JavaScript request into a script tag.

The following code loads my latest tweets into the page. Notice the call to jQuery's getJSON() method, and in particular the first argument. This is the address of the Twitter API for loading my tweets. If you want to load your own tweets, obviously swap 'lampmichael' for your own twitter handle. Also notice 'callback=?' at the end. This is the magic sauce. jQuery will replace the question mark with a dynamically generated function name and when the Twitter service responds to this request, it will call that function which in turn calls the anonymous function that is passed as the second argument to getJSON().

In the anonymous function, the tweets are simply dumped into the body tag within an unordered list. Each tweet has quite a lot of data. In the sample code I've left a commented out debugger statement. Remove the two slashes and load the page in Firefox with Firebug installed to see exactly what Twitter sends back.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-gb" lang="en-gb" dir="ltr" > 
<head> 
 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <title>Twitter JSONP</title>
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript"></script>
  <script type="text/javascript"><!--
  $(document).ready(function () {
    $.getJSON(
      'http://twitter.com/status/user_timeline/lampmichael.json?count=10&callback=?',
      function(data) {
        if (data.length>0) {
          $('body').html('<ol id="tweets"></ol>');
    //debugger;
          data.forEach(function(datum) {
            $('#tweets').append('<li>'+datum.text+' ('+datum.source+')</li>');
          });
        }
      }
    );
  });
  //--></script>
  <style type="text/css">
  #tweets {
    max-width: 400px;
    list-style:none
  }
  #tweets li {
 margin-bottom: 10px;
 padding: 2px
  }
  #tweets li:nth-child(odd) {
   background-color:#EEE
  }
  </style>
</head>
<body>
</body>
</html>

As this is just HTML and JavaScript, save the code into a plain old HTML file and load it in your browser right off your desktop; there's no need to serve it from IIS or Apache.