Implementing Zend Layout and Smarty using Zend Framework MVC
12 03 2008Lately I have received some requests on how Zend Layout can be used while not using the default templating system in Zend Framework (ZF), but instead using Smarty as a templating engine. So here is a write up on how to do so. The way to implement this may vary, so this is just one way of doing it.
This "tutorial" requires that you have at least some sense on how ZF works and a little bit on MVC in general.
1) Get Zend Studio for Eclipse as this will help A LOT on structuring your applications in a good way.
2) Document folder structure:
Having a good document folder structure will also help you build clean and maintainable applications. I try to mimic the ZF coding standard and naming conventions as much as possible. Here's my setup:
shared/ lib/ Zend/ Smarty/ mySiteName/ application/ default/ controllers/ models/ views/ filters/ helpers/ layouts/ default.tpl (layoutname).tpl scripts/ (controller)/ (action).tpl config/ config.xml data/ cache/ lib/ EZ/ View/ Smarty/ Plugin/ Abstract.php Broker.php Exception.php Standard.php Smarty.php public_html/ css/ js/ pics/ .htaccess index.php templates_c/
3) The config file
< ?xml version="1.0"?> <configdata> <production> <webhost>www.somesite.com</webhost> <database> <adapter>mysqli</adapter> <params> <host></host> <username></username> <password></password> <dbname></dbname> </params> </database> <libpath></libpath> <session_name>my_session</session_name> <base_url>http://www.somesite.com</base_url> <base_prefix>http://</base_prefix> <smarty> <compiledir></compiledir> <suffix>tpl</suffix> </smarty> <layout> <layoutpath></layoutpath> <contentkey>content</contentkey> <template>Special</template> <suffix>tpl</suffix> </layout> <debugging>false</debugging> <cache> <defaultlifetime>3600</defaultlifetime> <defaultcachedir>../data/cache/</defaultcachedir> </cache> </production> <staging extends="production"> <debugging>true</debugging> </staging> </configdata>
4) The bootstrap index.php
In order for things to work with Smarty and Zend Layout, we need to set som extra variables and inflector scripts so that view scripts will be resolved and parsed appropriately.
/** * ezApplicationBase - A Zend Framework implementation with Smarty and Zend Layout * * Bootstrap file for mysite.com * * @author Anders Fredriksson (http://anders.tyckr.com) * @version 0.1 */ //three folders "above" index.php define('ROOT_DIR', dirname(dirname(dirname(__FILE__)))); define('APP_DIR',dirname(dirname(__FILE__))); set_include_path('.' . PATH_SEPARATOR . APP_DIR . '/lib/' . PATH_SEPARATOR . APP_DIR . '/application/default/models/' . PATH_SEPARATOR . ROOT_DIR . '/shared/lib/' . PATH_SEPARATOR . get_include_path()); //This requires that your Zend library lies in ROOT_DIR/shared/lib/ //make classes autoload without doing require require_once('Zend/Loader.php'); Zend_Loader::registerAutoload(); if(defined('ENV') !== TRUE) { define('ENV','production'); //change staging to production to go to production settings } $config = new Zend_Config_Xml(APP_DIR . '/config/config.xml', ENV); Zend_Registry::set('config',$config); //init session $session = new Zend_Session_Namespace($config->session_name); Zend_Registry::set('session',$session); Zend_Db_Table::setDefaultAdapter(Zend_Db::factory(Zend_Registry::get('config')->database)); /** * Init the Smarty view wrapper and set smarty suffix to the view scripts. */ $view = new EZ_View_Smarty($config->smarty->toArray()); //use the viewrenderer to keep the code DRY //instantiate and add the helper in one go $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('ViewRenderer'); $viewRenderer->setView($view); $viewRenderer->setViewSuffix($config->smarty->suffix); /** * Set inflector for Zend_Layout */ $inflector = new Zend_Filter_Inflector(':script.:suffix'); $inflector->addRules(array(':script' => array('Word_CamelCaseToDash', 'StringToLower'), 'suffix' => $config->layout->suffix)); // Initialise Zend_Layout's MVC helpers Zend_Layout::startMvc(array('layoutPath' => ROOT_DIR.$config->layout->layoutPath, 'view' => $view, 'contentKey' => $config->layout->contentKey, 'inflector' => $inflector)); $front = Zend_Controller_Front::getInstance(); $front->setControllerDirectory(array( 'default' => '../application/default/controllers', 'blog' => '../application/blog/controllers', )); $front->throwExceptions(true); //enable logging to default.log $writer = new Zend_Log_Writer_Stream(APP_DIR.'/data/log/default.log'); $logger = new Zend_Log($writer); //give easy access to the logger Zend_Registry::set('logger',$logger); try { $front->dispatch(); } catch(Exception $e) { echo nl2br($e->__toString()); }
5) The Smarty implementation of Zend_View. For this I want to give Naneau credit as it is mostly the same as the implementation he did a while back.
EZ/View/Smarty.php
/** * * @since Dec 7 2007 * @author Anders Fredriksson */ /** * Zend View Base Class */ require_once 'Zend/View.php'; /** * Smarty templating engine */ require_once 'Smarty/Smarty.class.php'; /** * @category */ class EZ_View_Smarty extends Zend_View_Abstract { /** * Smarty object * @var Smarty */ protected $_smarty; protected $_plugins; /** * Constructor * * Pass it a an array with the following configuration options: * * scriptPath: the directory where your templates reside * compileDir: the directory where you want your compiled templates (must be * writable by the webserver) * configDir: the directory where your configuration files reside * * both scriptPath and compileDir are mandatory options, as Smarty needs * them. You can't set a cacheDir, if you want caching use Zend_Cache * instead, adding caching to the view explicitly would alter behaviour * from Zend_View. * * @see Zend_View::__construct * @param array $config ["scriptPath" => /path/to/templates, * "compileDir" => /path/to/compileDir, * "configDir" => /path/to/configDir ] * @throws Exception */ public function __construct($config = array()) { $this->_smarty = new Smarty ( ); //smarty object if (! array_key_exists ( 'compileDir', $config )) { throw new Exception ( 'compileDir must be set in $config for ' . get_class ( $this ) ); } else { $this->_smarty->compile_dir = $config ['compileDir']; } //compile dir must be set $this->_smarty->debugging = true; if (array_key_exists ( 'configDir', $config )) { $this->_smarty->config_dir = $config ['configDir']; } //configuration files directory parent::__construct ( $config ); //call parent constructor $this->_plugins = new EZ_View_Smarty_Plugin_Broker($this); $this->registerPlugin(new EZ_View_Smarty_Plugin_Standard()); } public function setCompilePath($dir) { $this->_smarty->compile_dir = $dir; return $this; } /** * Return the template engine object * * @return Smarty */ public function getEngine() { return $this->_smarty; } /** * register a new plugin * * @param EZ_View_Smarty_Plugin_Abstract */ public function registerPlugin(EZ_View_Smarty_Plugin_Abstract $plugin,$stackIndex = null) { $this->_plugins->registerPlugin ( $plugin, $stackIndex ); return $this; } /** * Unregister a plugin * * @param string|EZ_View_Smarty_Plugin_Abstract $plugin Plugin object or class name */ public function unRegisterPlugin($plugin) { $this->_plugins->registerPlugin ( $plugin ); return $this; } /** * fetch a template, echos the result, * * @see Zend_View_Abstract::render() * @param string $name the template * @return void */ protected function _run() { $this->strictVars ( true ); $vars = get_object_vars ( $this ); foreach ( $vars as $key => $value ) { if ('_' != substr ( $key, 0, 1 )) { $this->_smarty->assign ( $key, $value ); } } //assign variables to the template engine $this->_smarty->assign_by_ref ( 'this', $this ); //why 'this'? //to emulate standard zend view functionality //doesn't mess up smarty in any way $path = $this->getScriptPaths (); $file = substr ( func_get_arg ( 0 ), strlen ( $path [0] ) ); //smarty needs a template_dir, and can only use templates, //found in that directory, so we have to strip it from the filename $this->_smarty->template_dir = $path [0]; //set the template diretory as the first directory from the path echo $this->_smarty->fetch ( $file ); //process the template (and filter the output) } }
My additions are basically the possibility to add plugins to Smarty without putting them in the Smarty lib directory. This is needed so that a nicer implementation of ZF's view helpers can be used.
6) Smarty plugin loader
EZ/View/Smarty/Plugin/Standard.php
class EZ_View_Smarty_Plugin_Standard extends EZ_View_Smarty_Plugin_Abstract { public static function layoutFunction($params, &$smarty) { $section = $params ['section']; $toReturn = ""; try { //oppress errors @$toReturn = $smarty->_tpl_vars ['this']->placeholder ( 'Zend_Layout' )->$section; return $toReturn; } catch ( Exception $e ) { return $toReturn; } } public static function headTitleFunction($params, &$smarty) { $method = (isset ( $params ['method'] )) ? $params ['method'] : - 1; unset ( $params ['method'] ); $args = (isset ( $params ['args'] )) ? self::parseArgs ( $params ['args'] ) : null; $toReturn = ""; try { //oppress errors if ($method != - 1) { @$toReturn = $smarty->_tpl_vars ['this']->headTitle ()->$method ( $args ); } else { $toReturn = $smarty->_tpl_vars ['this']->headTitle (); } return $toReturn; } catch ( Exception $e ) { return $toReturn; } } public static function headScriptFunction($params, &$smarty) { $method = (isset ( $params ['method'] )) ? $params ['method'] : - 1; unset ( $params ['method'] ); $args = (isset ( $params ['args'] )) ? self::parseArgs ( $params ['args'] ) : null; $toReturn = ""; try { if ($method != - 1) { //oppress errors if (is_array ( $args )) { //$toReturn = call_user_func('$smarty->_tpl_vars["this"]->headScript()->$method()',$args,true); //print "args src= ".$args['src']; //TODO: fix args usage $toReturn = $smarty->_tpl_vars ['this']->headScript ()->$method ( $args ['src'] ); } else { @$toReturn = $smarty->_tpl_vars ['this']->headScript ()->$method ( $args ); } } else { $toReturn = $smarty->_tpl_vars ['this']->headScript (); } return $toReturn; } catch ( Exception $e ) { return $toReturn; } } public static function jsLocaleFunction($params, &$smarty) { $method = (isset ( $params ['method'] )) ? $params ['method'] : - 1; unset ( $params ['method'] ); $args = (isset ( $params ['args'] )) ? self::parseArgs ( $params ['args'] ) : null; $toReturn = ""; try { if ($method != - 1) { //oppress errors if (is_array ( $args )) { //$toReturn = call_user_func('$smarty->_tpl_vars["this"]->headScript()->$method()',$args,true); //print "args src= ".$args['src']; //TODO: fix args usage $toReturn = $smarty->_tpl_vars ['this']->jsLocale ()->$method ( $args ['src'] ); } else { @$toReturn = $smarty->_tpl_vars ['this']->jsLocale ()->$method ( $args ); } } else { $toReturn = $smarty->_tpl_vars ['this']->jsLocale (); } return $toReturn; } catch ( Exception $e ) { return $toReturn; } } public static function headStyleFunction($params, &$smarty) { $method = (isset ( $params ['method'] )) ? $params ['method'] : - 1; unset ( $params ['method'] ); $args = (isset ( $params ['args'] )) ? self::parseArgs ( $params ['args'] ) : null; $toReturn = ""; try { if ($method != - 1) { //oppress errors if (is_array ( $args )) { //$toReturn = call_user_func('$smarty->_tpl_vars["this"]->headStyle()->$method()',$args,true); //print "args src= ".$args['src']; //TODO: fix args usage $toReturn = $smarty->_tpl_vars ['this']->headStyle ()->$method ( $args ['src'] ); } else { @$toReturn = $smarty->_tpl_vars ['this']->headStyle ()->$method ( $args ); } } else { $toReturn = $smarty->_tpl_vars ['this']->headStyle (); } return $toReturn; } catch ( Exception $e ) { return $toReturn; } } public static function parseArgs($args) { if (preg_match_all ( "/([^\\[^\\]]+)/", $args, $matches )) { $params = explode ( ',', $matches [1] [0] ); $toReturn = array ( ); foreach ( $params as $p ) { @list ( $key, $value ) = explode ( '=>', $p ); $toReturn [preg_replace ( "/[\\' ]+/", "", $key )] = preg_replace ( "/[\\' ]+/", "", $value ); } return $toReturn; } return $args; } /** * Smarty block function, provides gettext support for smarty. * * The block content is the text that should be translated. * * Any parameter that is sent to the function will be represented as %n in the translation text, * where n is 1 for the first parameter. The following parameters are reserved: * - escape - sets escape mode: * - 'html' for HTML escaping, this is the default. * - 'js' for javascript escaping. * - 'url' for url escaping. * - 'no'/'off'/0 - turns off escaping * - plural - The plural version of the text (2nd parameter of ngettext()) * - count - The item count for plural mode (3rd parameter of ngettext()) */ public static function tBlock($params, $text, &$smarty) { $text = stripslashes ( $text ); // set escape mode if (isset ( $params ['escape'] )) { $escape = $params ['escape']; unset ( $params ['escape'] ); } // set plural version if (isset ( $params ['plural'] )) { $plural = $params ['plural']; unset ( $params ['plural'] ); // set count if (isset ( $params ['count'] )) { $count = $params ['count']; unset ( $params ['count'] ); } } // use plural if required parameters are set if (isset ( $count ) && isset ( $plural )) { $text = EZ_Language::getInstance ()->translatePlural ( $text, $plural, $count ); } else { // use normal $text = EZ_Language::getInstance ()->translate ( $text ); } // run strarg if there are parameters if (count ( $params )) { $text = self::strarg ( $text, $params ); } if (! isset ( $escape ) || $escape == 'html') { // html escape, default $text = nl2br ( htmlspecialchars ( $text ) ); } elseif (isset ( $escape )) { switch ( $escape) { case 'javascript' : case 'js' : // javascript escape $text = str_replace ( '\'', '\\\'', stripslashes ( $text ) ); break; case 'url' : // url escape $text = urlencode ( $text ); break; } } return $text; } /** * Replaces arguments in a string with their values. * Arguments are represented by % followed by their number. * * @param string Source string * @param mixed Arguments, can be passed in an array or through single variables. * @returns string Modified string */ public static function strarg($str) { $tr = array(); $p = 0; for ($i=1; $i < func_num_args(); $i++) { $arg = func_get_arg($i); if (is_array($arg)) { foreach ($arg as $aarg) { $tr['%'.++$p] = $aarg; } } else { $tr['%'.++$p] = $arg; } } return strtr($str, $tr); } }
This file is basically to allow for view helper function calls that are needed to get Zend Layout to work properly. The Smarty compiler has a limitation of just allowing one level of function calls {$this->someHelper()} works, but {$this->someHelper()->someHelperFunction()} does not work. Zend Layout requires you to do $this->placeholder("Zend_Layout")->section to output the data inside the layoutsection "section". With my helper the syntax for that would be {layout section='section'}
The other helpers in there are some of the standard helpers that comes with Zend View. I kept them in there to give you an idea of what can be done.
The main purpose for using Zend Layout with Smarty is that you can give your site a clean look without having to duplicate code.
7) The default template for the layout. I use YUI Grids CSS to get a good clean standard css layout.
<xml version="1.0" encoding="UTF-8" />
{$this->doctype()}
<html xmlns='http://www.w3.org/1999/xhtml' xml:lang="en" lang="en">
<head>
{headTitle}
{headScript}
{$this->headLink()}
<link rel="stylesheet" href="http://yui.yahooapis.com/2.4.1/build/reset-fonts-grids/reset-fonts-grids.css" type="text/css">
{headStyle}
{$this->headMeta()}
{*debugging turned on as a default during development *}
{if $debugging}
{debug}
{/if}
</link></head>
<body>
<div id="custom-doc" class="yui-t2">
<div id="hd" class="header">{$this->placeholder("header")}</div>
<div id="bd">
<div id="yui-main">
<div class="yui-b">
<div class="yui-g">
<!-- YOUR DATA GOES HERE -->
{layout section="content"}
</div>
</div>
</div>
<div id="leftColumn" class="yui-b"><!-- YOUR NAVIGATION GOES HERE -->
{$this->placeholder("leftColumn")}
</div>
</div>
<div id="ft">{$this->placeholder("footer")}</div>
</div>
</body>
</html>
Get some action to render into a section, eg. "leftColumn".
For this to work with a Zend Layout section you need to set the responseSegmentName of that action, which is done with:
$this->_helper->viewRenderer->setResponseSegment('section');
Doing this will probably put you in a rough spot when you try to make calls to specific actions from within other actions or preDispatch. To avoid this there is a simpler method that I just switched to.
To render action "testAction()" from OtherController into a "section" leftColumn for instance, and to do that from another action call "indexAction()" we do the following:
public function indexAction() { $this->view->placeholder('leftColumn')->set($this->view->action('test','Other')); $this->view->someVar = "this will get rendered into the index.tpl"; }
And that pretty much wraps it up for now.
Please feel free to ask me any questions.
Update: I guess it will help more if I post the code for the Smarty plugin helpers
EZ/View/Smarty/Plugin/Abstract.php
abstract class EZ_View_Smarty_Plugin_Abstract { protected $_functionRegistry; protected $_namingPattern ='/([a-zA-Z1-9]+)(Function|Block)$/'; /** * */ function __construct() { } /** * @return array */ public function getClassFunctionArray() { $type = get_class($this); $methods = get_class_methods($this); foreach($methods as $value) { if(preg_match($this->_namingPattern,$value,$matches)) { $this->_functionRegistry[$matches[1]] = $type."::".$value; } } return $this->_functionRegistry; } /** * change the default naming pattern for functions that should be mapped to smarty functions */ public function setNamingPattern($pattern ='/([a-zA-Z1-9]+)(Function|Block)$/') { //"/([a-zA-Z1-9]+)Function$/"; $this->_namingPattern = $pattern; } }
EZ/View/Smarty/Plugin/Broker.php:
/** * EZ_View_Smarty_Plugin_Broker * * This class registers smarty plugins with the current smarty view */ class EZ_View_Smarty_Plugin_Broker { /** * Array of instance of objects extending EZ_View_Smarty_Plugin_Abstract * * @var array */ protected $_plugins = array ( ); protected $_view; /** * * @param EZ_View_Smarty $view */ public function __construct($view) { $this->_view = $view; } /** * Register a plugin. * * @param EZ_View_Smarty_Plugin_Abstract $plugin * @param int $stackIndex * @return EZ_View_Smarty_Plugin_Broker */ public function registerPlugin(EZ_View_Smarty_Plugin_Abstract $plugin,$stackIndex = null) { if (false !== array_search ( $plugin, $this->_plugins, true )) { throw new EZ_View_Smarty_Exception ( 'Plugin already registered' ); } $stackIndex = ( int ) $stackIndex; if ($stackIndex) { if (isset ( $this->_plugins [$stackIndex] )) { throw new EZ_View_Smarty_Exception ( 'Plugin with stackIndex "' . $stackIndex . '" already registered' ); } $this->_plugins [$stackIndex] = $plugin; } else { $stackIndex = count ( $this->_plugins ); while ( isset ( $this->_plugins [$stackIndex] ) ) { ++ $stackIndex; } $this->_plugins [$stackIndex] = $plugin; } ksort ( $this->_plugins ); $this->reLoadPlugins(); return $this; } /** * Unregister a plugin. * * @param string|EZ_View_Smarty_Plugin_Abstract $plugin Plugin object or class name * @return EZ_View_Smarty_Plugin_Broker */ public function unregisterPlugin($plugin) { if ($plugin instanceof EZ_View_Smarty_Plugin_Abstract) { // Given a plugin object, find it in the array $key = array_search ( $plugin, $this->_plugins, true ); if (false === $key) { throw new EZ_View_Smarty_Exception ( 'Plugin never registered.' ); } unset ( $this->_plugins [$key] ); } elseif (is_string ( $plugin )) { // Given a plugin class, find all plugins of that class and unset them foreach ( $this->_plugins as $key => $_plugin ) { $type = get_class ( $_plugin ); if ($plugin == $type) { unset ( $this->_plugins [$key] ); } } } $this->reLoadPlugins(); return $this; } /** * Is a plugin of a particular class registered? * * @param string $class * @return bool */ public function hasPlugin($class) { $found = array ( ); foreach ( $this->_plugins as $plugin ) { $type = get_class ( $plugin ); if ($class == $type) { return true; } } return false; } /** * Retrieve a plugin or plugins by class * * @param string $class Class name of plugin(s) desired * @return false|EZ_View_Smarty_Plugin_Abstract|array Returns false if none found, plugin if only one found, and array of plugins if multiple plugins of same class found */ public function getPlugin($class) { $found = array ( ); foreach ( $this->_plugins as $plugin ) { $type = get_class ( $plugin ); if ($class == $type) { $found [] = $plugin; } } switch ( count ( $found )) { case 0 : return false; case 1 : return $found [0]; default : return $found; } } /** * Retrieve all plugins * * @return array */ public function getPlugins() { return $this->_plugins; } /** * Load all plugins */ public function loadPlugins() { foreach($this->_plugins as $plugin) { $type = get_class($plugin); //get an array of all the classfunctions that should be mapped to smarty functions $functions = $plugin->getClassFunctionArray($type); foreach($functions as $key => $value) { if(preg_match('/Block$/',$value)) { $this->_view->getEngine()->register_block($key,$value); } else { $this->_view->getEngine()->register_function($key,$value); } } } } public function unLoadPlugins() { foreach($this->_plugins as $plugin) { $type = get_class($plugin); //get an array of all the classfunctions that should be mapped to smarty functions $functions = $plugin->getClassFunctionArray($type); foreach($functions as $key => $value) { if(preg_match('/Block$/',$value)) { $this->_view->getEngine()->unregister_block($key); } else { $this->_view->getEngine()->unregister_function($key); } } } } public function reLoadPlugins() { $this->unLoadPlugins(); $this->loadPlugins(); } }
Now, that should do it.
Categories : programming
