Creating a themable application in Zend

We are using the Zend Framework as the basis for list8D and whilst Zend has a good view layer it does not support having multiple themes, not out of the box anyway. Luckily the classes responsible for the view have some pretty extensive methods for controlling where to look for templates and code. I couldn’t find any good documentation on using these to create a multiple theme application, so here is how I have set up list8D to not only support multiple themes but allow these themes to inherit each other.

Unfortunately the code blocks on the blog are a bit hard to read, but hopefully this post will give you the gist. If you want to have a look at the actually code feel free to have a browse of our svn on Google Code.

How it works!

Structure of a theme

Themes are contained within a single directory, this includes all the CSS, JavaScript and images. You will need to create an alias to your themes directory in your public directory so that your CSS, JS and images are under the web root.

The structure of a themes directory looks a like this:

/themes
/themename
/css – contains the themes CSS files
/helpers – contains view helper classes
/images – contains images
/layouts – The page template file, a single file:
/default.tpl.php
/templates – The view template files, following the default format:
/controller-action.tpl.php
/root.info – theme definition, contains things like what style sheets to load

TODO: This setup does not support JavaScript yet, but you the method would be the same as for CSS.

How does inheritance work?

A theme may extend another, this is defined in the theme’s .info file with the extend setting. If a file, whether a stylesheet, image, template or class, is not available the theming system will look to the themes parent theme for the file.

Note: Images referenced in stylesheets will not be searched for in this fashion.

Themes inheritance is chained, so the theming system will continue to look for a file up the chain until it reaches a theme that does not extend another.

list8D library

We wanted to keep our base classes outside the application so created a list8D folder inside /library, this is home to the List8D_ namespace but as long as your autoloader is set up correctly your base classes can be anywhere.

Custom base controller

We are going to make most of the changes to the view object in the controller, so rather than repeat these method calls for each of our controllers I set up a custom base controller class to execute all these changes in it’s init method. All our actual controllers extend this one so they too will make the needed changes to the view object.

First things first, open the php, class, and init method, this base controller will extend the zend base controller so that we don’t lose any of it’s functionality:

<?php
class List8D_Controller extends Zend_Controller_Action {
public function init() {

Some code to make the view renderer, layout and layout view quicker to access:

$this->layout = $this->_helper->getHelper('Layout')->getLayoutInstance();
$this->layoutView = $this->layout->getView();
$this->viewRenderer = $this->_helper->getHelper('viewRenderer');

We will configure the theme settings in the file /application/config/theme.ini, so let’s load that:

$themeSettings = new Zend_Config_Ini(APPLICATION_PATH."/configs/theme.ini");

In this file we can define the current theme, but we can override it with a url parameter that will be saved for this session:

$session = new Zend_Session_Namespace('Default');
if (isset($_GET['theme'])) {
$currentTheme = $_GET['theme'];
$session->theme = $_GET['theme'];
} else if ($session->theme) {
$currentTheme = $session->theme;
}

Themes are contained in a single directory and defined by a .info file inside that directory, so we’ll load the current theme’s info file:

$themeinfo = new Zend_Config_Ini(APPLICATION_PATH."/themes/".$currentTheme."/".$currentTheme.".info");
$currentThemeInfo = $themeinfo;

Themes can extend other themes so we also need to load the info file of the extended theme and that themes extended theme until we get to the root theme:

$themeinfos = array($currentTheme => $themeinfo);

while($themeinfo->extends) {

$key = $themeinfo->extends;
// load extended theme infos
$themeinfo = new Zend_Config_Ini(APPLICATION_PATH."/themes/".$themeinfo->extends."/".$themeinfo->extends.".info");
$themeinfos[$key] = $themeinfo;

}

Now we add the helper, template and layout directories for each of the themes in order of inheritance. This means if the current theme doesn’t implement one of these the view will look to the theme it extends until it finds the file it needs:

foreach(array_reverse($themeinfos) as $key => $themeinfo) {

// add view helper paths
$this->view->addHelperPath(APPLICATION_PATH . "/themes/".$key ."/helpers", 'List8D_Theme_'.str_replace(" ","",ucwords(str_replace("_"," ",$key)))."_Helper");

// add template paths
$this->view->addScriptPath(APPLICATION_PATH . "/themes/".$key ."/templates");

// add layout paths
$this->layout->getView()->addScriptPath(APPLICATION_PATH . "/themes/".$key ."/layouts");

}

Although we’ll still be able to add page specific css and javascript from the action methods we want to be able to define style sheets for all pages (for things like layout), we can do this in the current themes info file:


// what all css to load
if ($currentThemeInfo->get('css.all')) {
foreach($currentThemeInfo->get('css.all') as $css) {
$this->view->headLink()->appendStylesheet($css,'all');
}
}

if ($currentThemeInfo->get('css.screen')) {
// what screen css to load
foreach($currentThemeInfo->get('css.screen') as $css) {
$this->view->headLink()->appendStylesheet($css,'screen');
}
}

if ($currentThemeInfo->get('css.aural')) {
// what aural css to load
foreach($currentThemeInfo->get('css.aural') as $css) {
$this->view->headLink()->appendStylesheet($css,'aural');
}
}

TODO: There are still media types I need to add, primarily print and mobile. I also still need to add the ability to add JavaScript files in this way and settings to have conditional stylesheets for IE.

Although we have added the template, view and layout paths for the extended themes, we haven’t for the current theme, so let go ahead and do that:


// set paths
$tmplPath = APPLICATION_PATH . "/themes/".$themeSettings->theme."/templates" ;
$layoutPath = APPLICATION_PATH . "/themes/".$themeSettings->theme ."/layouts";

// add action helper path
Zend_Controller_Action_HelperBroker::addPath("List8D/helpers", 'List8D_Action_Helper');

$viewRenderer = $this->_helper->getHelper('viewRenderer');

$viewRenderer->setView($this->view);
$viewRenderer->setViewBasePathSpec($tmplPath)

The next few lines are more a point of personal preference, basically I’m going to remove the way that zend requires controller sub directories for templates and then change the template extension to .tpl.php


$viewRenderer->setViewScriptPathSpec(':controller-:action.:suffix')
$viewRenderer->setViewSuffix('tpl.php');

Now to change the default name and extension of layout files, to match template files:


$this->layout->setLayout("default");
$this->layout->setViewSuffix('tpl.php');

We want to define theme settings in the theme.ini file rather than hard coding it into the theme. This code will load them into the layout view.

// set layout variables
if ($themeSettings->variables) {
foreach($themeSettings->variables as $key => $variable) {
$this->layout->getView()->$key = $variable;
}
}

TODO: I’ll probably be adding the same variables to the view so that they are also available to action templates not just the layout. I’ll also be compressing them into an array so that they don’t interfere with other variables.

Custom headlink helper

The default Zend headlink helper allows us to add CSS and JavaScript to the head of the document as and when we need them. We are going to extend this so that you just need to pass it the filename and it will look in the right directory, iterating up the theme inheritance tree until it find the file it needs.


theme;
$session = new Zend_Session_Namespace('Default');
if (isset($_GET['theme'])) {
$currentTheme = $_GET['theme'];
$session->theme = $_GET['theme'];
} else if ($session->theme) {
$currentTheme = $session->theme;
}

$href_out = false;
// if current theme has css file use that
if (is_file(APPLICATION_PATH."/themes/".$currentTheme."/css/".$href)) {
$href_out = $this->view->baseUrl()."/themes/".$currentTheme."/css/".$href;
}

// otherwise look in each of the extended themes
else {
$themeinfo = new Zend_Config_Ini(APPLICATION_PATH."/themes/".$currentTheme."/".$currentTheme.".info");
while ($themeinfo->extends) {
if (is_file(APPLICATION_PATH."/themes/".$themeinfo->extends."/css/".$href)) {
$href_out = $this->view->baseUrl()."/themes/".$themeinfo->extends."/css/".$href;
}
$themeinfo = new Zend_Config_Ini(APPLICATION_PATH."/themes/".$themeinfo->extends."/".$themeinfo->extends.".info");
}
}

if ($href_out) {
$this->__call('appendStylesheet',array($href_out, $media, $conditionalStylesheet, $extras));
}
return $this;
}

}

I’ve added this file to my root themes helpers directory (ei /themes/root/helpers) this way as long as I don’t redefine it in my theme and my theme extends the root theme I will get this functionality. You will either need it in your theme or extend the root theme for the previous custom controller base to work properly.

Report post

Leave a Reply