Writing a CMS/Community with Smarty and the Zend Framework: Part 9

This piece covers creating a proper folder structure so that we can have a skin system. I’ve also created a folder for the admin section which is fully contained with it’s own controllers folder and so on. Another unrelated change that has happened since the last part is sub menu logic which we will also take a look at.

folder_struct.jpg

Let’s begin with the bootstrap file, somehow we have to let ZF know that we want to use two controller directories:

. . .
$frontController = Zend_Controller_Front::getInstance();
$frontController->throwExceptions(true);
$controllerDirectories = array('default' => './controllers', 'administrator' => './administrator/controllers');
$frontController->setControllerDirectory($controllerDirectories)
				->setRouter(new Zend_Controller_Router_Rewrite())
				->registerPlugin(new ControllerPlugin());
. . .

The only new thing here is that we now have two controller directories, our default one we’ve used all the time and /administrator/controllers. Let’s take a look at administrator/controllers/IndexController.php:

class Administrator_IndexController extends ExtControllerAdmin{
	
	function init(){
		parent::init();
		$this->name			= 'index';
		$this->module		= 'administrator';
		parent::finishInit();
	}
	
	function indexAction(){
		$this->display();
	}
	
	function dAction(){
		$params = $this->_request->getParams();
		$params['m'] = 'administrator';
		$this->assign('params', $params);
		$this->display();
	}
	
	function frontPage(){
		return $this->fetch('front_page.tpl');
	}
	
	function render($params){
		return $this->frontPage();
	}
		
	function norouteAction(){
		$this->refresh();
		exit;
	}
}

There are a couple of new things to note here. First we have to have a prefix in the class name which corresponds to the key ‘administrator’ in our $controllerDirectories variable in the bootstrap file, hence the name Administrator_IndexController. As you see we have a new extension to ExtController called ExtControllerAdmin which looks like this at the moment:

class ExtControllerAdmin extends ExtController{
	
	function init(){
		parent::init();
	}
	
	function finishInit(){
		parent::finishInit();
		$this->smarty->prependDirs("administrator");
		$this->assign('designDir', "administrator");
	}
}

It seems our Smarty class has got some new additions:

class Smarty_Zend extends Smarty{
   
   function __construct(){
		parent::Smarty();
		$this->template_dir 	= 'templates';
		$this->config_dir 		= 'configs';
		$this->compile_dir 		= 'templates_c';
		$this->cache_dir 		  = 'cache';
   }
   
   function setConfig($config = false){
   		if(!empty($config) && is_array($config)){
   			foreach($config as $key => $value)
   				$this->$key = $value;
   		}
   }
   
   function prependDirs($value = ""){
   	if(!empty($value)){
   	    $this->template_dir   = $value . '/' . $this->template_dir;
            $this->config_dir 	   = $value . '/' . $this->config_dir;
            $this->compile_dir    = $value . '/' . $this->compile_dir;
            $this->cache_dir 	  = $value . '/' . $this->cache_dir;
   	}
   }
}

There is not much to say here, we’ve got two new functions that will enable us to change the paths on the fly whenever we want. In the above case we would have a prior value of ‘templates’ in the template_dir for instance, which after passing ‘administrator’ to the prependDirs function will have a value of administrator/templates, easy stuff.

Let’s continue examining Administrator_IndexController, in the init() function we’ve got $this->module = ‘administrator’;. The new module variable is used in several places in ExtController:

function finishInit(){
  $this->assign('controller', $this->name);
  $this->template		= $this->name.".tpl";
  $this->base_url 	= $this->_request->getBaseUrl();
  
  if(!empty($this->module))
    $this->base_url .= "/".$this->module;
    
  $this->compl_path = "http://{$_SERVER['HTTP_HOST']}{$this->base_url}";
  $this->assign('baseUrl', $this->base_url);
  $this->assign('complUrl', $this->compl_path);
  $this->session = new Zend_Session_Namespace($this->name);
  $this->assign('resourceDir', "resources");
}

function loadModel($table){
  $class_name = strtolower($table);
  if(empty($this->module))
    $file_name = $class_name.".php";
  else
    $file_name = $this->module."/models/".$class_name.".php";
  include_once($file_name);
    
  return new $class_name;
}

We have to implement our new modular system in the base_url variable, we also have to account for it when we load models. On a side note, notice the new template variable we have called resourceDir. I’ve moved some menu related images from the image directory that the template design utilizes. Since menu pics are to be uploaded by admins for menus created on the fly we have two separate the folders. Think media manager in Joomla.

Back to Administrator_IndexController again, the next new thing is $params[’m'] = ‘administrator’; in dAction. This is a requirement to let our new modularity cascade through the logic of displaying various pieces. Let’s follow this to it’s conclusion by first taking a look at a section of administrator/templates/index/index.tpl:

. . .
<div id="main">
  <!-- begin: #col3 static column -->
  <div id="col3">
    <div id="col3_content" class="clearfix">
      <!-- skiplink anchor: Content -->
      {insert name="item" params=$params module="administrator"}
    </div>
    <div id="ie_clearing">&nbsp;</div>
    <!-- End: IE Column Clearing -->
  </div>
  <!-- end: #col3 -->
</div>
. . .

As you can see we pass our insert_item function a new key value pair of module => administrator. So we have to check out widgets.php to see what has happened:

function insert_item($params){
	if(empty($params['params'])){
		$function 	= empty($params['function']) ? 'render' : $params['function'];
		$controller = empty($params['controller']) ? 'index' : $params['controller'];
		$module 	  = empty($params['module']) ? '' : $params['module'];
		return Common::loadController($controller, false, $module)->$function($params);
	}else{
		$function 	= empty($params['params']['f']) ? 'render' : $params['params']['f'];
		$controller = empty($params['params']['c']) ? 'index' : $params['params']['c'];
		$module 	  = empty($params['params']['m']) ? '' : $params['params']['m'];
		return Common::loadController($controller, false, $module)->$function($params['params']);
	}
}

It makes sense, we have to keep track of the current module now, in addition to the controller and function. And as you can see the Common::loadController function takes a third argument now:

static function loadController($controller, $request = false, $module = ''){
  $controller = ucfirst($controller)."Controller";
  $module = ucfirst($module);
  $class_name = empty($module) ? $controller : $module."_".$controller;
  if(!class_exists($class_name)){
    $file_name = "controllers/".$class_name.".php";
    if(!empty($module))
      $file_name = "$module/$file_name";
    include_once($file_name);
  }
  return new $class_name($request);
}

In addition, both the display and fetch function in ExtController have been changed:

function display($template = false, $folder = false){
  $page = $this->fetch($template, $folder);
  echo $page;
  exit;
}

function fetch($template = false, $folder = false){
  if($template == false){
    $this->smarty->template_dir .= "/".$this->name;
    return utf8_encode($this->smarty->fetch($this->template));
  }else{
    if($folder !== null){
      if($folder == false)
        $this->smarty->template_dir .= "/".$this->name;
      else
        $this->smarty->template_dir .= "/".$folder;
    }
    return utf8_encode($this->smarty->fetch($template));
  }
}

We’re changing the smarty template directory to handle skinning now. To understand this completely we have to check out ExtControllerFront which all non admin controllers are an extension of nowadays:

class ExtControllerFront extends ExtController{
	
	function init(){
		parent::init();
	}
	
	function finishInit(){
		parent::finishInit();
		$this->setGlobalConfig('mainconfig');
		$this->config_file	= "{$this->gs->gc->front_language}_{$this->name}.conf";
		$this->assign('config_file', $this->config_file);
		$templ_dir = "templates/{$this->gs->gc->design}";
		$this->smarty->setConfig(array("template_dir" => $templ_dir));
		$this->assign('designDir', "{$this->base_url}/$templ_dir");
		$this->assign('resourceDir', "{$this->base_url}/resources");
	}
}

Apparently we change the smarty template dir when we initialize, to “templates/{$this->gs->gc->design}”. $this->gs->gc->design is set when we call $this->setGlobalConfig(’mainconfig’);:

function setGlobalConfig($table){
  if(!isset($this->gs->gc))
    $this->gs->gc = (object)$this->loadModel($table)->fetchAllKeyValue();
}

We basically load the whole main config table straight into an array. Changes to the config table since last time is a name change from language to language_front for the language field. However, the most important change is the ‘design’ field which has a value of ‘default’ at the moment. This setup reflects the folder structure where we currently have all our templates, it’s of course templates/default/*/*.tpl. Remember that $this->gs is a session variable, that is why we can check if it is set or not, it stays put between requests. A final note; you might be wondering about the designDir variable, that is just a copy of the template dir in the Smarty object, we use it to load css at the moment. Yes I have thrown in the css, YAML and images folder among the templates too now. It would be a little hard to implement self contained skins otherwise.

Let’s continue with the new submenu logic like I promised in the beginning. First of all the db_menuitems table has got a new field called submenu_id. If an item has a submenu_id that is not 0 we will treat it as being a headline which will hide/unhide a submenu. Let’s begin with menu.tpl:

<script src="{$baseUrl}/js/rollOverImage.js" language="JavaScript"></script>
<SCRIPT type="text/javascript" src="{$baseUrl}/js/common.js"></SCRIPT>
.
.
.
{elseif $menu.menu_type eq "vertical"}
	<ul class="{$menu.menu_class}" id="{$submenu_id}">
	{foreach from=$menuitems item=menu_item}
		{if empty($menu_item.item_class)}
			<li class="{$menu.menu_itemclass}">
		{else}
			<li class="{$menu_item.item_class}">
		{/if}
		
		{if empty($menu_item.item_imageover) == true and $menu_item.submenu_id == 0}
			<a href="{$baseUrl}/{$menu_item.item_link}">
		{elseif $menu_item.submenu_id == 0}
			<a href="{$baseUrl}/{$menu_item.item_link}" onmouseout="MM_swapImgRestore()" onmouseover="MM_swapImage('image-{$menu_item.id}','','{$resourceDir}/{$menu_item.item_imageover}',1)">
		{else}
			<a href="javascript:toggleViz('{$menu_item.submenu_id}')">
		{/if}
		
		{if empty($menu_item.item_image) == false}
			<img id="image-{$menu_item.id}" src="{$resourceDir}/{$menu_item.item_image}" alt="{$menu_item.image_alt}" />
		{/if}
		{if empty($menu_item.link_text) == false}
			<span>{$menu_item.link_text}</span>
		{/if}
		</a>
		{if $menu_item.submenu_id != 0}
			{insert name="item" controller="Menu" function="subMenu" id=$menu_item.submenu_id}
		{/if}
		</li>
	{/foreach}
	</ul>

The new common.js only contains toggleViz at the moment:

function toggleViz(el_id){ 
    $("#"+el_id).toggleClass("hide_me"); 
}

As you can see we’ve only implemented the new sub menu logic for vertical menus so far. Also notice the new resource directory in action when we retrieve images. Anyway, the most important thing is the new insert we perform if $menu_item.submenu_id is not 0. Let’s take a look at the target function in MenuController:

function subMenu($params){
  $menu = $this->obj->fetchRow("id = {$params['id']}");
  $this->assign('submenu_id', $params['id']);
  return $this->getMenu($menu);
}

function getMenu($menu){
  $menu_items = $menu->findDependentRowset('menuitem', 'db_menu')->toArray();
  $menu_items = $this->filterItems($menu_items);
  Common::sort2DAsc($menu_items, 'position');
  $this->assign_by_ref('menu', $menu->toArray());
  $this->assign_by_ref('menuitems', $menu_items);
  return $this->fetch();
}

If there is a menu item in this sub menu with a submenu_id that is not 0 we will go through this again and again to draw the whole menu tree. This is essentially a recursive process which is really the best way of handling arbitrary trees. However, don’t make menus with a depth of more than some 100-200, otherwise PHP will get grumpy on you :)

Related Posts

Tags: , , , ,