Zend Framework and Twig
Update It looks like I should keep a closer eye on the mailing lists for new projects. Some people have already started to look into integrating Twig with the Zend Framework. The thread can be found in the group here.
I have a small confession to make. Whilst I do love the Zend Framework I have been seeing other frameworks and indeed languages behind it's back. One of these frameworks include Django for Python. Admittedly every time I start up in a new framework I do always seem to come back to the Zend Framework fold but I guess that must be down to the fact that I just prefer PHP, it's where I feel comfortable.
There was one feature that stuck with me about Django though, and that was it's template language. I found this to be very enjoyable to do, and to be honest in my view of things using PHP as my template language wasn't cutting it anymore in my books. I suspect that some people will strongly disagree with my arguments but then again this is what I feel is the beauty of PHP, there are so many tools out there that I can make them fit with my preferred style.
Fortunately the Symfony crowd have come to the rescue with, what I think is, a nice little template language called Twig. It resembles the style of templates found in Django with curly braces all over the place rather than PHP tags. Plus it lets you include blocks of templates in with other templates, this appealed to me because I found that [sourcecode language="php"]<?php echo $this->render("view-file.phtml"); ?>[/sourcecode] was getting too much for me. Additionally since the template language is very similar in both form and function to the Django template language anyone coming over shouldn't have any problems settling in on the template front.
I have been getting stuck into more features of the Zend Framework and thought that it would be fun to write an integration layer so that Twig can be used in Zend Framework. The solution I came up with works for default views and views contained inside modules.
To start with I created a wrapper around the main Twig_Environment class. This class is responsible for loading a template and then rendering it with the supplied data.
[sourcecode language="php"]
<?php
class ZExt_Twig extends Zend_View_Abstract
{
private $_twig = null;
public function __construct($applicationRoot, $modulesDir = "")
{
$twigLoader = new ZExt_Twig_Loader($applicationRoot, $modulesDir);
$this->_twig = new Twig_Environment($twigLoader);
}
protected function _run()
{
$template = func_get_arg(0);
echo $template->render($this->getVars());
}
protected function _script($name)
{
return $this->_twig->loadTemplate($name)
}
}
[/sourcecode]
I will come onto the ZExt_Twig_Loader class shortly. The two methods _run and _script are both overriden methods from Zend_View_Abstract. These get called when the render method of the parent class get's called. The _script function is responsible for loading the appropriate Twig template and then returning it.
The _run method is called when we are ready to hand off to the Twig rendering engine. The first argument to this method should be the template object that was returned by the _script method. Note that when the render method is being called we are passing in an array of any data that might have been assigned to the view.
Next the ZExt_Twig_Loader class is responsible for loading the appropriate template file from the standard Zend Framework application directory structure.
[sourcecode language="php"]
<?php
class ZExt_Twig_Loader extends Twig_Loader
{
/**
* @var string
*/
private $_applicationRoot = null;
/**
* @var string
*/
private $_modulesDir = null;
/**
* @var string
*/
private $_viewsDir = null;
/**
* Constructor.
*
* @param string $applicationRoot The application root.
* @param string $modulesDir The directory where all modules are found.
* @param string $viewsDir The path where views should be located in both the application root and
* modules directory.
* @param string $cache The compiler cache directory.
* @param boolean $autoReload Whether to reload the template if the orginal source changed.
*
* @see Twig_Loader
*/
public function __construct($applicationRoot, $modulesDir, $viewsDir = "/views/scripts",
$cache = null, $autoReload = true)
{
$this->setApplicationRoot($applicationRoot);
$this->setModulesDir($modulesDir);
$this->setViewsDir($viewsDir);
parent::__construct($cache, $autoReload);
}
/**
* Returns the source of the next view to load based on the supplied name.
* The name in this instance should take the form 'module/controller/action',
* 'controller/action', or just 'action'.
*
* @param string $name The view to load.
*
* @return array An array consisting of the source code as the first element, and
* the last modification time as the second one or false if it's not relevant.
*/
public function getSource($name)
{
$name = $this->_parseName($name);
$templateFilePath = $this->getApplicationRoot();
if (!is_null($name['module']))
{
$templateFilePath .= DIRECTORY_SEPARATOR.$name['module'];
}
$templateFilePath .= DIRECTORY_SEPARATOR.$this->getViewsDir();
$templateFilePath .= DIRECTORY_SEPARATOR.$name['controller'].DIRECTORY_SEPARATOR.$name['action'].$name['extension'];
if (!file_exists($templateFilePath))
{
throw new Zend_Exception("The template file $templateFilePath does not exist.");
}
error_log($templateFilePath);
return array(file_get_contents($templateFilePath), filemtime($templateFilePath));
}
public function getViewsDir()
{
return $this->_viewsDir;
}
public function setViewsDir($viewsDir)
{
$this->_viewsDir = $viewsDir;
}
public function getApplicationRoot()
{
return $this->_applicationRoot;
}
public function setApplicationRoot($applicationRoot)
{
$this->_applicationRoot = $applicationRoot;
}
public function getModulesDir()
{
return $this->_modulesDir;
}
public function setModulesDir($modulesDir)
{
$this->_modulesDir = $modulesDir;
}
private function _parseName($name)
{
$matches = array();
preg_match("/(\..*)$/", $name, $matches);
$name = substr($name, 0, strpos($name, $matches[1]));
$parsedName = array('module' => null, 'controller' => 'index', 'action' => 'index', 'extension' => $matches[1]);
$parts = split("/", $name);
switch (sizeof($parts))
{
case 3:
$parsedName['module'] = array_shift($parts);
case 2:
$parsedName['controller'] = array_shift($parts);
case 1:
$parsedName['action'] = array_shift($parts);
}
return $parsedName;
}
}
[/sourcecode]
The main method of interest from this class is ZExt_Twig_Loader::getSource. This method is called by the Twig environment when ZExt_Twig::loadTemplate is called. This function then parses the file name passed to it and returns an array containing an array of all the lines in the template file and the time the file was last modified. The template is then parsed by the Twig template engine.
Finally to get this all working I added the following function to the bootstrap
[sourcecode language="php"]
public function _initTwigView()
{
$twigView = new ZExt_Twig(APPLICATION_PATH, APPLICATION_PATH."/modules");
$viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper("ViewRenderer");
$viewRenderer->setView($twigView);
return $twigView;
}
[/sourcecode]
This now means that a Twig template view will be loaded instead of the default Zend_View style template.
There are still a few aspects I have to try out with this approach such as the availability of view helpers and whether I can include other templates using the Twig engine syntax, or if I will still have to use Zend Framework helpers to achieve this. Finally I need to tidy up some of the method signatures as there are some such as the ZExt_Twig_Loader constructor that have parameters which cannot be assigned.
All of the code in this article is available on GitHub.
Any suggestions for improvements would be appreciated :)