Ok, we have a very rough base up and running, however, without navigation a site is useless, as is it without at least marginal SEO, in this post we will add the basics of Navigation, Blocks and a little SEO which we actually already started but never focused on in the page manager section.
Phase 1: Navigation,
Instead of re-inventing the wheel, lets use what is already available, Zend_Navigation…. this will allow us to generate our menu’s with little to no code.
first lets edit Ig_Application.php and add the following methods
[php]
function generateMenus()
{
$acl = Zend_Registry::get("ACL");
$menu_model = new Menus();
$menus = $menu_model->getMenus();
foreach($menus as $menu)
{
$menuBuilderArray[$menu['id']] = array(‘label’ => $menu['menuname'],’title’=>$menu['menuname'], ‘controller’=>’index’,
‘pages’ => $this->_menuGenerate($menu_model,0,$menu['id']));
//$menuBuilderArray[$item['menu_id']]['pages'] = $this->_menuGenerate($menu_model,0,array());
}
return $menuBuilderArray;
}
private function _menuGenerate($menu_model, $parent_id, $menuid)
{
$acl = Zend_Registry::get(‘ACL’);
$items = $menu_model->getMenuItems($parent_id, $menuid);
$menuArray = array();
foreach($items as $item)
{
$acl->add(new Zend_Acl_Resource(‘menuitem:’.$item['id']));
$acl->allow($item['role'],’menuitem:’.$item['id']);
$params = array();
if($item['params'] != ” && $item['module'] != ”)
{
if($item['params'][0] == ‘/’) $item['params'] = substr($item['params'], 1);
$exploded = explode("/", $item['params']);
$paramcnt = count($exploded);
$x = 0;
while($x <= $paramcnt)
{
$params[$exploded[$x]] = $exploded[$x+1];
$x = $x+2;
}
}
if($item['module'] == ‘default’ && $item['controller'] == ‘page’)
{
$tlabel = str_replace(" ","_", $item['label']);
$params['p'] = preg_replace("/[^a-zA-Z0-9_\d]/i", "",$tlabel);
}
if($item['module'] != ”)
{
$menuArray[$item['id']] = array(
‘label’ => $item['label'],
‘title’=>$item['label'],
‘module’=>$item['module'],
‘controller’=>$item['controller'],
‘action’=>$item['action'],
‘params’=> $params,
‘resource’ => ‘menuitem:’.$item['id'],
‘pages’=>$this->_menuGenerate($menu_model, $item['id'],$menuid));
}else{
$menuArray[$item['id']] = array(
‘label’ => $item['label'],
‘title’ => $item['title'],
‘uri’ => $item['params'],
‘resource’ => ‘menuitem:’.$item['id'],
‘pages’=>$this->_menuGenerate($menu_model, $item['id'],$menuid));
}
}
return $menuArray;
}
[/php]
we also need to add the following lines to the initialise method
[php]
$menu = $this->generateMenus();
$container = new Zend_Navigation($menu);
Zend_Registry::set(‘Zend_Navigation’, $container);
[/php]
the following database structure
[sql]
CREATE TABLE IF NOT EXISTS `menuitems` (
`id` int(11) NOT NULL auto_increment,
`menu_id` int(11) NOT NULL,
`label` varchar(200) NOT NULL,
`module` varchar(200) NOT NULL,
`controller` varchar(200) NOT NULL,
`action` varchar(200) NOT NULL,
`params` varchar(200) NOT NULL,
`role` varchar(200) NOT NULL,
`ordering` int(11) NOT NULL,
`parent_id` int(11) NOT NULL default ’0′,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=17 ;
INSERT INTO `menuitems` (`id`, `menu_id`, `label`, `module`, `controller`, `action`, `params`, `role`, `ordering`, `parent_id`) VALUES
(1, 1, ‘Home’, ‘default’, ‘index’, ‘index’, ”, ‘Guests’, 0, 0),
(2, 2, ‘Side Home’, ‘default’, ‘index’, ‘index’, ”, ‘Guests’, 0, 0),
(6, 1, ‘Admin’, ‘admin’, ‘index’, ‘index’, ”, ‘Admins’, 5, 0),
(7, 2, ‘Test”s Item’, ‘default’, ‘page’, ‘view’, ‘id/1′, ‘Guests’, 4, 0),
(8, 1, ‘Externaltest’, ”, ”, ”, ‘http://www.jhsa.co.za’, ‘Members’, 44, 0),
(9, 1, ‘Logout’, ‘user’, ‘profile’, ‘logout’, ”, ‘Members’, 999, 0),
(10, 3, ‘Admin Home’, ‘admin’, ‘index’, ‘index’, ”, ‘Admins’, 1, 0),
(11, 3, ‘Main Site’, ‘default’, ‘index’, ‘index’, ”, ‘Admins’, 0, 0),
(12, 3, ‘Content Pages’, ‘admin’, ‘pages’, ‘index’, ”, ‘Admins’, 3, 0),
(13, 3, ‘Menu Manager’, ‘admin’, ‘menu’, ‘index’, ”, ‘Admins’, 4, 0),
(14, 3, ‘Modules’, ‘admin’, ‘modules’, ‘index’, ”, ‘Admins’, 5, 0),
(15, 3, ‘Logout’, ‘user’, ‘profile’, ‘logout’, ”, ‘Admins’, 99, 0),
(16, 1, ‘My Account’, ‘user’, ‘index’, ‘index’, ”, ‘Guests’, 2, 0);
CREATE TABLE IF NOT EXISTS `menus` (
`id` int(11) NOT NULL auto_increment,
`menuname` varchar(200) NOT NULL,
`role` varchar(200) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;
INSERT INTO `menus` (`id`, `menuname`, `role`) VALUES
(1, ‘MENUTOP’, ‘Guests’),
(2, ‘MENUSIDE’, ‘Members’),
(3, ‘MENUADMIN’, ‘admins’);
[/sql]
The edit our Ig/Controller/Action.php init method with the following lines of code:
[php]
$view->navigation()->setAcl($acl)->setRole($role);
[/php]
And our layout.phtml (front / admin) depending on which menu you want to display at the given point
[php]
<?php
$menu = $this->navigation()->findOneByLabel(‘MENUSIDE’);
echo $this->navigation()->menu()->setMaxDepth(4)->renderMenu($menu); ?>
[/php]
In the block of code in our view, we basically set it up to grab the menu based on the label, set a maximum depth, this can be ommited or set to 0, if ommited is unlimited depth, if set to 0 will only return the top level items.
our menu is outputted into an unordered list and with a little css styling can be converted into displaing the way you would like it to.
Phase 2:
Blocks:: Called modules in Joomla, These blocks are mini sections, such as the Get Updated section on the right of this post, they need to have the following features:
positionable by position, one or more blocks per position with ordering, also they should be access controled based on role.
To pull this off we need yet another View Helper, so lets get to coding it.
[php]
<?php
class Ig_View_Helper_DrawBlock extends Zend_View_Helper_Abstract
{
public $params;
public function __construct()
{
$front = Zend_Controller_Front::getInstance();
$modules = $front->getControllerDirectory();
if (empty($modules))
{
require_once ‘Zend/View/Exception.php’;
throw new Zend_View_Exception(‘DrawBlock helper depends on valid front controller instance’);
}
$request = $front->getRequest();
$response = $front->getResponse();
if (empty($request) || empty($response)) {
require_once ‘Zend/View/Exception.php’;
throw new Zend_View_Exception(‘DrawBlock view helper requires both a registered request and response object in the front controller instance’);
}
$auth = Zend_Auth::getInstance();
$this->params->request = clone $request;
$this->params->response = clone $response;
$this->params->dispatcher = clone $front->getDispatcher();
$this->params->defaultModule = $front->getDefaultModule();
$this->params->acl = Zend_Registry::get("ACL");
$this->params->user = $auth->getIdentity();
$this->db = Zend_Registry::get("db");
}
function drawBlock($pos)
{
$blocks = $this->db->fetchAll("Select b.* from blocks as b left join block_positions as bp on b.block_position_id = bp.id where bp.blockName = ‘$pos’ and b.published = 1 order by ordering asc");
$xhtml = ‘<div id="’ . strtolower($pos) . ‘_block_container">’;
foreach($blocks as $block)
{
//block_title show_title
$xhtml .= ‘<li>’;
if($block['show_title'] == 1) {
$xhtml .= ‘<h2>’ . $block['block_title'] . ‘</h2>’;
}
$params = $this->params;
$params->params = $block['params'];
$class = new $block['ClassFile']();
$xhtml .= $class->drawBlock($params, $pos);
$xhtml .= ‘</li>’;
}
$xhtml .= ‘</div>’;
return $xhtml;
}
}
[/php]
our database
[sql]
CREATE TABLE IF NOT EXISTS `blocks` (
`id` int(11) NOT NULL auto_increment,
`block_position_id` int(11) NOT NULL,
`ClassFile` varchar(200) NOT NULL,
`role` varchar(50) NOT NULL,
`published` int(1) NOT NULL,
`params` text NOT NULL,
`ordering` int(11) NOT NULL default ’0′,
`block_title` varchar(200) NOT NULL,
`show_title` int(1) NOT NULL default ’0′,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;
INSERT INTO `blocks` (`id`, `block_position_id`, `ClassFile`, `role`, `published`, `params`, `ordering`, `block_title`, `show_title`) VALUES
(1, 1, ‘Block_Login’, ‘Guests’, 1, ”, 0, ‘Members’, 1);
CREATE TABLE IF NOT EXISTS `block_positions` (
`id` int(11) NOT NULL auto_increment,
`blockName` varchar(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=6 ;
INSERT INTO `block_positions` (`id`, `blockName`) VALUES
(1, ‘Left’),
(2, ‘Right’),
(3, ‘Top’),
(4, ‘Header’),
(5, ‘Footer’);
[/sql]
Now as you an see we have one block already loaded into the database, here is the code for it, this will add a login block to the left block position if not logged in else a logut link if you are…..
[php]
<?php
//modules/user/Block/Login.php
class Block_Login
{
public function __construct()
{
}
public function drawBlock($params, $block)
{
if($params->user->id < 1)
{
return $this->drawLogin($params, $block);
}else{
return $this->drawLogout($params,$block);
}
}
public function drawLogout($params, $block)
{
$user = $params->user;
$html = ”;
$html .= ‘<div class="’ . strtolower($block) . ‘_block"><ul>
Welcome ‘ . $user->username . ‘</li>
<li><a href="/user/profile/edit">Edit Profile</a></li>
<li><A href="/user/profile/logout" >Logout</a></li></ul></div>’;
return $html;
}
public function drawLogin($params, $block)
{
$html = ”;
$html .= ‘<div class="’ . strtolower($block) . ‘_block"><Form action="/user/login/" method="post">Username:<br /><input type="text" name="username"><br />Password:<br /><input type="password" name="password"><br /><input type="submit" value="Login"></form>
<ul><li><a href="/user/profile/forgot">Forgot Password</a></li>
<li><a href="/user/profile/create">Create Account</a></li></ul></div>’;
return $html;
}
}
[/php]
and in our layout.phtml file
[php]
<?php
echo $this->drawBlock(‘Left’);
?>
[/php]
where you want the blocks to appear. in order to keep the system easy and mostly controlled by the css you will see that i use the position aswell as the _block as a class / id tag so that styling can be done within the css.
Now once we start getting to advanced designes we might wish to have conditional display, ie if there is a left block display it along with a container around it, if not do not even output the container.
to do this we modified the drawBlock Class drawBlock method as follows
[php]
function drawBlock($pos, $getCount = false)
{
$blocks = $this->db->fetchAll("Select b.* from blocks as b left join block_positions as bp on b.block_position_id = bp.id where bp.blockName = ‘$pos’ and b.published = 1 order by ordering asc");
if($getCount)
{
return count($blocks);
}
//… balance remained the same.
[/php]
Ok now for our little SEO, You will see that in our earlier page we had the keywords and description stored in the database with each page, making sure that your template has the following in the head section
[php]
<?php echo $this->headTitle() ?>
<?php echo $this->headMeta() ?>
[/php]
within our controllers we can use
[php]
$this->view->headMeta()->appendName(‘keywords’, $page['meta_keywords']);
$this->view->headMeta()->appendName(‘description’, $page['meta_description']);
$this->view->headTitle($page['title']);
[/php]
Now with that we would obviously like to have some default data accross all pages, such as our site name etc to do this i created a config table as follows
[sql]
CREATE TABLE IF NOT EXISTS `siteconfig` (
`id` int(11) NOT NULL auto_increment,
`cfgkey` varchar(250) NOT NULL,
`cfgval` varchar(250) NOT NULL,
`cfgdesc` varchar(250) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;
–
– Dumping data for table `siteconfig`
–
INSERT INTO `siteconfig` (`id`, `cfgkey`, `cfgval`, `cfgdesc`) VALUES
(1, ‘SITE_TITLE’, ‘IGCMS’, ‘Main Page Title’),
(2, ‘SITE_KEYWORDS’, ‘CMS’, ‘Site Keywords’),
(3, ‘SITE_DESC’, ‘Zend Framework based site’, ‘Main meta description’);
[/sql]
create a model in my models directory called Config.php
[php]
class Config extends Database
{
function get($cfgkey)
{
return $this->db->fetchOne("Select cfgval from siteconfig where cfgkey = ‘$cfgkey’");
}
}
[/php]
and finaly edited my Ig/Controller/Action.php file as follows
added this to the init method
[php]
$this->_loadConfig();
[/php]
modified the _loadConfig method to look as follows
[php]
function _loadConfig()
{
$cfg = new Config();
$this->view->headTitle()->setSeparator(‘ :: ‘);
$this->view->headTitle($cfg->get(‘SITE_TITLE’));
$this->view->headMeta()->appendName(‘keywords’,$cfg->get(‘SITE_KEYWORDS’));
$this->view->headMeta()->appendName(‘description’, $cfg->get(‘SITE_DESC’));
}
[/php]
Ok next up: what use is a site with ACL, Login when no-one can register, also we need to setup a language system, aaah i see the focus of my next 2 posts, registration, user activation and management and translation / muliti-lingual sites.!!!!
See you soon…
ps. I will be launching the demo of this site in a few days and will post the link here, as always the code is in the svn….
Great tutorial. I really like your idea for blocks. I was thinking of how to implement the db schema for this, yours seems just right. I might add an extra field to the table ‘blocks’ maybe something along the lines of [module VARCHAR(100) NOT NULL default 'defaultmodulename'] to add module specific rendering of these blocks. so when we iterate over blocks of a position we also ensure they are supposed to be displayed for the current module.
@kayani, By having each Block setup correctly in the blocks folder of the module, it should not be needed to define which module the block belongs to, as each classname should be unique, the sytem will look through each module till it finds the correct block,and instantiates that class, however, should you find that adding the module name to the table increases the performance or makes the system more expandable please let us know.
I like this handling of Blocks.
To improve on how you implement your blocks, I would create a Block interface to enforce available functions in Block class implementations.
In your class ‘Ig_View_Helper_DrawBlock’, I don’t think $this->db is declared anywhere ($params is declared but not $db). Just a best practices thing.