Chapter 7. Creating the Site Navigation

Creating a clean design that catches your visitor's attention and writing compelling content are inarguably critical keys of a high-quality website, but without efficient navigation, your site will flounder. The attention span of a typical web surfer is measured in seconds, so if they are not able to find the information they are looking for at a glance, they will almost instantly click away to a competitor's site. When you are developing a CMS, you need to develop tools that make it easy and intuitive for your site managers to build and manage efficient menus.

How CMSs Manage Menus

You have a range of options when managing navigation, ranging from blog systems that automate the entire process following a linear convention that people have learned to low-level CMSs that require you to write custom code to create the menus. Each of these extremes has pros and cons.

The blog approach takes navigation out of the hands of the writer so they do not have to worry about it. This works only for a site that follows the dictated convention, so this approach lacks the flexibility that many sites require.

The low-level CMSs give an experienced developer ultimate control over how the navigation works. The issue with this approach is that it usually requires some expertise to update and manage the navigation.

Like most software solutions, your CMS will be a compromise between these two extremes. It will not automate the menu creation but will make it easy for anyone to add pages to the menus. It will also allow you to add static links to the menu, which will be used to add links to the modules that you develop later in this book as well as the site administration functions.

Managing Menu Data

Every time I build a CMS project, I finish the content management piece and figure that the hard part is behind me, only to be surprised by how much work goes into navigation. Managing navigation is at least as complicated as managing content.

This CMS will use two tables to manage menus:

  • menus: This table will contain the menu metadata, which for the time being will just be the name and the access level.

  • menu_items: This table will contain the menu items with their labels and links.

To get started, create these two tables using the SQL statements in Listing 7-1 and Listing 7-2.

Example 7.1. The SQL Statement to Create the menus Table

CREATE TABLE menus (
  id int(11) NOT NULL auto_increment,
  name varchar(50) default NULL,
  access_level varchar(50) default NULL,
  PRIMARY KEY  (id)
) AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

Example 7.2. The SQL Statement to Create the menu_items Table

CREATE TABLE menu_items (
  id int(11) NOT NULL auto_increment,
  menu_id int(11) default NULL,
  label varchar(250) default NULL,
  page_id int(11) default NULL,
  link varchar(250) default NULL,
  position int(11) default NULL,
  PRIMARY KEY  (id)
) AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

Next you need to create a model for each of these tables. You will add the methods as you develop this component, so for now, just create the model classes.

The menu to menu item relationship is a straightforward one-to-many relationship, which you can model with Zend_Db_Table_Relationship.

Create two new files in the application/models folder: Menu.php and MenuItem.php. Then define the classes, their associated table names, and the model relationships, as shown in Listing 7-3 and Listing 7-4.

Example 7.3. The Menu Model Class in application/models/Menu.php

<?php
require_once 'Zend/Db/Table/Abstract.php';
class Model_Menu extends Zend_Db_Table_Abstract {
      protected $_name = 'menus';
      protected $_dependentTables = array('Model_MenuItem'),
      protected $_referenceMap    = array(
        'Menu' => array(
            'columns'           => array('parent_id'),
            'refTableClass'     => 'Model_Menu',
            'refColumns'        => array('id'),
            'onDelete'          => self::CASCADE,
            'onUpdate'          => self::RESTRICT
        )
    );
}
?>

Example 7.4. The MenuItem Model Class in application/models/MenuItem.php

<?php
require_once 'Zend/Db/Table/Abstract.php';
class Model_MenuItem extends Zend_Db_Table_Abstract {
    protected $_name = 'menu_items';
    protected $_referenceMap    = array(
        'Menu' => array(
            'columns'           => array('menu_id'),
            'refTableClass'     => 'Model_Menu',
            'refColumns'        => array('id'),
            'onDelete'          => self::CASCADE,
            'onUpdate'          => self::RESTRICT
        )
    );

}
?>

Now that you have a basis for managing the menu data, you can get started.

Note

If you would like more information about how the Zend_Db_Table relationships work, please review chapter 5.

Creating the Menu Controllers

You will need two controllers for the menus, one for each model class. Some people prefer to centralize related components like these into a single controller, but I find it easier to manage if they are one-to-one (one controller for each model/table).

You can create these controllers manually or use Zend_Tool. If you use Zend_Tool, then navigate to the root of your project. Then call the create controller command for each of the controllers you need to create. See Listing 7-5 for the complete commands.

Example 7.5. The Zend_Tool Commands to Create the Menu and Menu Item Controllers

zf create controller menu
zf create controller menuitem

Zend_Tool creates the controller files and classes (see Listings 7-6 and 7-7), as well as the view folders for these controllers.

Example 7.6. The Menu Controller in application/controllers/MenuController.php

<?php
class MenuController extends Zend_Controller_Action
{
    public function init()
    {
        /* Initialize action controller here */
    }

    public function indexAction()
    {
        // action body
    }
}

Example 7.7. The Menu Item Controller in application/controllers/MenuitemController.php

<?php
class MenuitemController extends Zend_Controller_Action
{
    public function init()
    {
        /* Initialize action controller here */
    }

    public function indexAction()
    {
        // action body
    }
}

Now that the base controllers, tables, and models are set up, you are ready to get started managing the menus.

Creating a New Menu

The menu management will follow the now-familiar routine of stepping through the CRUD process.

Creating the Menu Form

The next step is to create the menu form. The menu form will be very simple; you need only a hidden field for the ID and text field for the menu name. Create a new file in application/forms named Menu.php. Create a new form for the menu, as shown in Listing 7-8.

Example 7.8. The Menu Form in application/forms/Menu.php

<?php
class Form_Menu extends Zend_Form
{
    public function init()
    {
        $this->setMethod('post'),

        // create new element
        $id = $this->createElement('hidden', 'id'),
        // element options
        $id->setDecorators(array('ViewHelper'));
        // add the element to the form
        $this->addElement($id);

        // create new element
        $name = $this->createElement('text', 'name'),
        // element options
        $name->setLabel('Name: '),
        $name->setRequired(TRUE);
        $name->setAttrib('size',40);
        // strip all tags from the menu name for security purposes
        $name->addFilter('StripTags'),
        // add the element to the form
        $this->addElement($name);

        $submit = $this->addElement('submit', 'submit', array('label' => 'Submit'));

    }
}
?>

Rendering the Create Menu Form

Now you need to create a new action in the menu controller to create a menu. You can do this with Zend_Tool using the create action command, as shown in Listing 7-9.

Example 7.9. Creating the Create Menu Action Using Zend_Tool

zf create action create menu

This will create the createAction() method in the menu controller as well as the view script for this action. Update the createAction() method to create a new instance of the menu form, set its action to create, and pass this to the view (see Listing 7-10).

Example 7.10. The Updated createAction() Method in application/controllers/MenuController.php

public function createAction()
{
    $frmMenu = new Form_Menu();
    $frmMenu->setAction('/menu/create'),
    $this->view->form = $frmMenu;
}

Now you need to render the form. Open the application/views/scripts/menu/create.phtml file. Add a descriptive headline to the page, and render the form, as shown in Listing 7-11.

Example 7.11. Rendering the Create Page Form in application/views/scripts/menu/create.phtml

<h2>Create a new menu</h2>
<p>To create a new menu complete this form and click submit...</p>
<?php echo $this->form; ?>

Now if you point your browser to http://localhost/menu/create, you should see the create menu page. This page should render your menu form, as shown in Figure 7-1.

The create menu page

Figure 7.1. The create menu page

Processing the Form

Now that the form is set up and rendering, you need to set up the back end to create the new menu. Start by adding a method to the Menu model named createMenu(), which will create a new row in the menu table, set the name, and save it, as shown in Listing 7-12.

Example 7.12. The createMenu() Method in application/models/Menu.php

public function createMenu($name)
{
    $row = $this->createRow();
    $row->name = $name;
    return $row->save();
}

With this method created, you are ready to process the form and create the new menu. The create page form will post back to the createAction() method in the MenuController class, so you will need to update this method to create the menu when the request is a postback, as shown in Listing 7-13. You will do this in much the same way as you created pages. If the page request is a postback, then you need to validate the form data. If it is valid, then create the menu. Once the menu is created, you will forward the user to the indexAction() method, which will list all the menus with management links. You will update this method next.

Example 7.13. The Updated createAction() in application/controllers/MenuController.php

public function createAction()
{
    $frmMenu = new Form_Menu();
    if($this->getRequest()->isPost()) {
        if($frmMenu->isValid($_POST)) {
            $menuName = $frmMenu->getValue('name'),
            $mdlMenu = new Model_Menu();
            $result = $mdlMenu->createMenu($menuName);
            if($result) {
                // redirect to the index action
                $this->_redirect('/menu/index'),
            }
        }
    }
    $frmMenu->setAction('/menu/create'),
    $this->view->form = $frmMenu;
}

Listing Current Menus

Now that you can create a new menu, you need a way to manage the menus. You will do this by creating a list of the current menus, with links to update the menu, manage the menu items, and delete the menu. Since this will be the main menu management page, you should add this to the indexAction() method. This method will need to fetch all the current menus and then pass them to the view to render.

Before you create the indexAction() method, you will need to create a method in the Menu model to get all the current menus. Add a new method to the Menu model named getMenus(). This method should sort the menus by name and return the Zend_Db_Table_Rowset of the results, as shown in Listing 7-14. Note that I like to evaluate whether there are any results and return null if not.

Example 7.14. The getMenus() Method in application/models/Menu.php

public function getMenus()
{
    $select = $this->select();
    $select->order('name'),
    $menus = $this->fetchAll($select);
    if($menus->count() > 0) {
        return $menus;
    }else{
        return null;
    }
}

Now update the indexAction() method. This method should create a new instance of the Menu model and pass the results of the getMenus() method to the view to render, as shown in Listing 7-15.

Example 7.15. The indexAction() in application/controllers/MenuController.php

public function indexAction()
{
    $mdlMenu = new Model_Menu();
    $this->view->menus = $mdlMenu->getMenus();
}

Next create a new view script in application/views/scripts/menu named index.phtml. Open this view script, and set the page title and headline as usual. Then check to see whether there are any menus currently. If there are menus, then create a table to list them, with a column for the edit, open, and delete linksand a column for the name. Use the partialLoop() helper to render the table rows (you will create the partial in one moment). If there aren't any menus, then just display a message to that effect. You should also add a link on the bottom to add a new menu. You can see the complete view script in Listing 7-16.

Example 7.16. The Menu Admin List in application/views/scripts/menu/index.phtml

<h2>Current Menus</h2>
<?php if($this->menus != null) { ?>
<table class='spreadsheet' cellpadding='0' cellspacing='0'>
    <tr>
        <th>Links</th>
        <th>Menu Name</th>
    </tr>
    <?php echo $this->partialLoop('partials/_menu-row.phtml', $this->menus); ?>
</table>
<?php }else{?>
<p>You do not have any menus yet.</p>
<?php }?>
<p><a href='/menu/create'>Create a new menu</a></p>

Now create the partial script to render the menu row. Create a new f file in the partials folder named _menu-row.phtml. This file will render the table row for the menu, with links to edit it, manage its items, and delete it, as shown in Listing 7-17.

Example 7.17. The Menu Row Partial in application/views/scripts /partials_menu-row.phtml

<tr>
    <td class='links'>
        <a href='/menu/edit/id/<?php echo $this->id;?>'>Edit</a> |
        <a href='/menuitem/index/menu/<?php echo $this->id;?>'>
            Manage Menu Items</a> |
        <a href='/menu/delete/id/<?php echo $this->id;?>'>Delete</a>
    </td>
    <td><?php echo $this->name ?></td>
</tr>

When you have completed this, you should be able to point your browser to http://localhost/menu and see a list of the menus, as shown in Figure 7-2.

The current menu list

Figure 7.2. The current menu list

Updating a Menu

Now you need to create the updateAction() method in the menu controller. This action will use the existing menu form but will differ from the createAction() method in that it needs to load the menu and populate the form prior to rendering the form.

Opening the Menu to Edit

The first thing you are going to need to do is create an edit action in the menu controller. You can create the edit action method, along with its view script, using Zend_Tool, as shown in Listing 7-18.

Example 7.18. Creating the Menu Controller Edit Action with Zend_Tool

zf create action edit menu

Now open the editAction() method in your editor. You are passing this function the ID of the menu to edit as a URL parameter, so you can fetch this using the request object. Next create an instance of the menu form and model. Finally, fetch the menu row, populate the form, and pass it to the view to render, as shown in Listing 7-19.

Example 7.19. Loading the Menu Form in application/controllers/MenuController.php

public function editAction()
{
    $id = $this->_request->getParam('id'),
    $mdlMenu = new Model_Menu();
    $frmMenu = new Form_Menu();
    // fetch the current menu from the db
    $currentMenu = $mdlMenu->find($id)->current();
    // populate the form
    $frmMenu->getElement('id')->setValue($currentMenu->id);
    $frmMenu->getElement('name')->setValue($currentMenu->name);
    $frmMenu->setAction('/menu/edit'),
    // pass the form to the view to render
    $this->view->form = $frmMenu;
}

Zend_Tool created the edit view script for you in application/views/scripts/menu/edit.phtml. Open this file, add a headline and instructions to it, and then render the form, as shown in Listing 7-20.

Example 7.20. The Edit Menu View Script in application/views/scripts/menu/edit.phtml

<h2>Update menu</h2>
<p>To update this menu complete this form and click submit...</p>
<?php echo $this->form; ?>

Updating the Menu

Now you need to process the form and update the menu. The first thing you need to do is add an updateMenu() method to the menu model. This method will find the menu row and update it (in this case, just update the name and then save it), as shown in Listing 7-21.

Example 7.21. The updateMenu() Method in application/models/Menu.php

public function updateMenu ($id, $name)
{
    $currentMenu = $this->find($id)->current();
    if ($currentMenu) {
        $currentMenu->name = $name;
        return $currentMenu->save();
    } else {
        return false;
    }
    }

Now you are ready to update the menus. The menu form will post back to the edit action, just as the create action did. You will need to update this action to process the form on the postback. When the form is posted back, you validate the form data and then update the menu if it passes the validation (see Listing 7-22).

Example 7.22. The Updated editAction() in application/controllers/MenuController.php

public function editAction()
{
    $id = $this->_request->getParam('id'),
    $mdlMenu = new Model_Menu();
    $frmMenu = new Form_Menu();
    // if this is a postback, then process the form if valid
    if($this->getRequest()->isPost()) {
        if($frmMenu->isValid($_POST)) {
            $menuName = $frmMenu->getValue('name'),
            $mdlMenu = new Model_Menu();
            $result = $mdlMenu->updateMenu($id, $menuName);
            if($result) {
                // redirect to the index action
                return $this->_forward('index'),
            }
        }
    }else{
        // fetch the current menu from the db
        $currentMenu = $mdlMenu->find($id)->current();
        // populate the form
        $frmMenu->getElement('id')->setValue($currentMenu->id);
        $frmMenu->getElement('name')->setValue($currentMenu->name);
    }
    $frmMenu->setAction('/menu/edit'),
    // pass the form to the view to render
    $this->view->form = $frmMenu;
}

Deleting Menus

Finally, you need a method to delete an existing menu. This will be very straightforward because it does not require a view, just the deleteAction() in the MenuController and a delete method in the Menu model.

To get started, create the deleteMenu() method in the Menu model. This method will try to find the menu, and if it is successful, it will delete the menu. If not, it will throw an exception (see Listing 7-23).

Example 7.23. The deleteMenu() Method in application/models/Menu.php

public function deleteMenu($menuId)
{
    $row = $this->find($menuId)->current();
    if($row) {
        return $row->delete();
    }else{
        throw new Zend_Exception("Error loading menu");
    }
}

Next create the deleteAction() in the MenuController. To do this with Zend_Tool, use the command in Listing 7-24.

Example 7.24. Creating the Delete Action in the Menu Controller with Zend_Tool

zf create action delete menu

Now you need to update the delete action that you just created. You need to fetch the id parameter, create a new instance of the menu model, and run the deleteMenu() method. Once it deletes the menu, it will forward to the admin list in the indexAction(), as shown in Listing 7-25.

Example 7.25. The deleteAction() in application/controllers/MenuController.php

public function deleteAction()
{
    $id = $this->_request->getParam('id'),
    $mdlMenu = new Model_Menu();
    $mdlMenu->deleteMenu($id);
    $this->_forward('index'),
}

Managing Menu Items

Menus are not much use without menu items. Managing menu items is somewhat more complicated than managing the menus themselves because the site administrator needs more control over them. She needs to be able to define menu labels, create links to menu pages and static links to modules, and control the order in which the menu items are displayed.

Listing the Menu Items

The first thing you are going to want to do is open a menu and create a list of the items in the menu (even though there will not be any yet). If you recall, there was a third link in the admin menu list, which pointed to the index action in the MenuItemController. This action will open the menu and then fetch all menu items, passing this information to the view.

To get started, open the menu model and create a method to fetch all the items from a menu named getItems(). Note that you could simply use the Zend_Db_Table_Relationship instance's findDependentRowset() method, but it does not offer the flexibility that you need to sort and conditionally display items. Use the Zend_Db_Table's select() object instead so you can programmatically build this query as needed. For now, add a WHERE clause to get only the items that are for the menu that was passed, and sort the items by position, as shown in Listing 7-26.

Example 7.26. The getItemsByMenu() Method in application/models/MenuItem.php

public function getItemsByMenu($menuId)
{
    $select = $this->select();
    $select->where("menu_id = ?", $menuId);
    $select->order("position");
    $items = $this->fetchAll($select);
    if($items->count() > 0) {
        return $items;
    }else{
        return null;
    }
}

Now that this method is created, you need to update the indexAction() to the MenuItemController. This action will get the menu that was passed via the URL parameter, create instances of the Menu and MenuItem models, and then pass the menu details and the menu items to the view, as shown in Listing 7-27.

Example 7.27. The indexAction() Method in application/controllers/MenuItemController.php

public function indexAction()
{
   $menu = $this->_request->getParam('menu'),
   $mdlMenu = new Model_Menu();
   $mdlMenuItem = new Model_MenuItem();
   $this->view->menu = $mdlMenu->find($menu)->current();
   $this->view->items = $mdlMenuItem->getItemsByMenu($menu);
}

Next update the view script. Open application/views/scripts/menuitem/index.phtml, and set the page title and headline. Then show the user which menu they are working with. Next check and see whether there are any menu items. If there are some, then use the partialLoop() helper to render them. If not, display a message letting the user know that there are no items in the menu yet. Finally, add a link to add a new item to the menu. Listing 7-28 shows the complete view script.

Example 7.28. The Menu Item List in application/views/scripts/menuitem/index.phtml

<h2>Manage Menu Items</h2>
<p>Current Menu: <?php echo $this->menu->name;?></p>
<?php
if($this->items != null) {
?>
<table>
    <tr>
        <th>Links </th>
        <th>Label</th>
        <th>Page</th>
        <th>Link</th>
    </tr>
    <?php echo $this->partialLoop('partials/_menu-item-row.phtml',
        $this->items); ?>
</table>
<?php }else{?>
<p>This menu does not have any items yet.</p>
<?php }?>

<p><a href='/menuitem/add/menu/<?php echo $this->menu->id ?>'>Add a new item</a></p>

Now create the partial script to render the menu item row. This will be very similar to the partial script you created for the menus, with one exception; you need to add links to move the menu item up or down in the list.

Add a new file named _menu-item-row.phtml to the partials folder. Since only some of the menu items link to pages, you need to first check what kind of item it is. If it is a page link, then you need to load the page so you can display the title rather than the ID that the CMS uses to reference the item. Then render the table row. Create links to update the item, move it up or down, and delete it. Then render the item label, the page title (if it exists), and the link (see Listing 7-29).

Example 7.29. The Menu Item List in application/views/scripts/partials/_menu-item-row.phtml

<?php
    if($this->page_id > 0) {
        $page = new CMS_Content_Item_Page($this->page_id);
        $pageName = $page->name;
    }else{
        $pageName = null;
    }
?>
<tr>
    <td class='links'>
        <a href='/menuitem/update/id/<?php echo $this->id;?>'>Update</a> |
        <a href='/menuitem/move/direction/up/id/<?php echo $this->id;?>'>
           Move up</a> |
        <a href='/menuitem/move/direction/down/id/<?php echo $this->id;?>'>
           Move down</a> |
        <a href='/menuitem/delete/id/<?php echo $this->id;?>'>Delete</a>
    </td>
    <td><?php echo $this->label ?></td>
    <td><?php echo $pageName ?></td>
<td><?php echo $this->link ?></td>
</tr>

Adding New Menu Items

Now you need to create the method to create new menu items. This will be similar to the rest of the create actions, with the exception that you need to keep track of the parent menu ID.

The Menu Item Form

The menu item form requires two hidden fields, one for the item ID and one for the parent menu ID. It also requires a text field for the label, a select control for the page, and another text field for the link (which will be used if the page is not set). The only thing that is different with this form is that you need to load all the current pages as options for the select page control. Listing 7-30 shows the completed form.

Example 7.30. The Menu Item Form in application/forms/MenuItem.php

<?php
class Form_MenuItem extends Zend_Form
{
    public function init()
    {
        $this->setMethod('post'),

        // create new element
        $id = $this->createElement('hidden', 'id'),
        // element options
        $id->setDecorators(array('ViewHelper'));
        // add the element to the form
        $this->addElement($id);

        // create new element
        $menuId = $this->createElement('hidden', 'menu_id'),
        // element options
        $menuId->setDecorators(array('ViewHelper'));
        // add the element to the form
        $this->addElement($menuId);

        // create new element
        $label = $this->createElement('text', 'label'),
        // element options
        $label->setLabel('Label: '),
        $label->setRequired(TRUE);
        $label->addFilter('StripTags'),
        $label->setAttrib('size',40);
        // add the element to the form
        $this->addElement($label);
// create new element
        $pageId = $this->createElement('select', 'page_id'),
        // element options
        $pageId->setLabel('Select a page to link to: '),
        $pageId->setRequired(true);

        // populate this with the pages
        $mdlPage = new Model_Page();
        $pages = $mdlPage->fetchAll(null, 'name'),
        $pageId->addMultiOption(0, 'None'),
        if($pages->count() > 0) {
            foreach ($pages as $page) {
                $pageId->addMultiOption($page->id, $page->name);
            }
        }
        // add the element to the form
        $this->addElement($pageId);

        // create new element
        $link = $this->createElement('text', 'link'),
        // element options
        $link->setLabel('or specify a link: '),
        $link->setRequired(false);
        $link->setAttrib('size',50);
        // add the element to the form
        $this->addElement($link);

        $submit = $this->addElement('submit', 'submit', array('label' => 'Submit'));
    }
}
?>

Creating the Add Menu Item Action

Now add the addAction() method to the MenuitemController. You can do this with Zend_Tool, as demonstrated in Listing 7-31, or simply add this code manually.

Example 7.31. Creating the menuitem Controller Add Action using Zend_Tool

zf create action add menuitem

The add action should load the menu that was passed via the id URL parameter, load the form, and populate the form with the menu ID. Then pass the form to the view to render it, as shown in Listing 7-32.

Example 7.32. The addAction() in application/controllers/MenuItemController.php

public function addAction()
{
$menu = $this->_request->getParam('menu'),
   $mdlMenu = new Model_Menu();
   $this->view->menu = $mdlMenu->find($menu)->current();
   $frmMenuItem = new Form_MenuItem();
   $frmMenuItem->populate(array('menu_id' => $menu));
   $this->view->form = $frmMenuItem;
}

Rendering the Menu Item Form

Next you need to update the add menu item form. Open the application/views/scripts/menuitem/add.phtml file that Zend_Tool created, and set the page headline. Then display the current menu so the user can keep track of what they are working on and render the form, as shown in Listing 7-33.

Example 7.33. Rendering the cadd menu item form in application/views/scripts/menu-item/add.phtml

<h2>Add Menu Item</h2>
<p>Current Menu: <?php echo $this->menu->name;?></p>
<p>To add a menu item fill out the form below and click submit...</p>
<?php echo $this->form; ?>

Processing the Form

Now you need to process the create menu item form. Create a new method in the MenuItem model to add the menu item. This method will work much like the other add/create methods with two exceptions. First, it needs to enter the parent menu ID, and second, it needs to set the item's position (Listing 7-34). You will set up the methods to manage the menu position in one moment.

Example 7.34. The addItem() Method in application/models/MenuItem.php

public function addItem($menuId, $label, $pageId = 0, $link = null)
{
    $row = $this->createRow();
    $row->menu_id = $menuId;
    $row->label = $label;
    $row->page_id = $pageId;
    $row->link = $link;
    // note that you wil create the _getLastPosition method in Listing 7-36
    $row->position = $this->_getLastPosition($menuId) + 1;
    return $row->save();
}

Next update the addAction() method in the MenuItemController to process the form on the postback, as shown in Listing 7-35. It needs to go through the standard process of checking to see whether the form has been posted back and then validating the form data and creating the item if the data is OK. It then forwards back to the menu item list.

Example 7.35. The Updated addAction() in application/controllers/MenuitemController.php

public function addAction ()
{
    $menu = $this->_request->getParam('menu'),
    $mdlMenu = new Model_Menu();
    $this->view->menu = $mdlMenu->find($menu)->current();
    $frmMenuItem = new Form_MenuItem();
    if ($this->_request->isPost()) {
        if ($frmMenuItem->isValid($_POST)) {
            $data = $frmMenuItem->getValues();
            $mdlMenuItem = new Model_MenuItem();
            $mdlMenuItem->addItem($data['menu_id'], $data['label'],
                $data['page_id'], $data['link']);
            $this->_request->setParam('menu', $data['menu_id']);
            $this->_forward('index'),
        }
    }
    $frmMenuItem->populate(array('menu_id' => $menu));
    $this->view->form = $frmMenuItem;
}

Sorting Menu Items

One challenge with managing menus is sorting them. The MVC approach does make this somewhat cleaner by separating the sorting logic from the application's functionality.

To start with, create the methods that will perform the sorting in the MenuItem model. This will require three methods:

  • _getLastPosition(): This method will get the last (highest) position in the selected menu and is a utility method that the MenuItem model uses (as in the addItem() method).

  • moveUp(): This method moves the selected menu item up.

  • moveDown(): This method moves the selected menu item down.

The first method you will create is the _getLastPosition() method, because the other methods will require it to function. This method is a straightforward function that uses the Zend_Db_Table's select() object to sort the items by position (descending) and filter them by the parent menu ID. It then uses the fetchRow() method to fetch a single row, which is the highest. If there are no results, then it returns 0. Listing 7-36 shows the complete method.

Example 7.36. The _getLastPosition() Method in application/models/MenuItem.php

private function _getLastPosition ($menuId)
    {
        $select = $this->select();
        $select->where("menu_id = ?", $menuId);
        $select->order('position DESC'),
        $row = $this->fetchRow($select);
if ($row) {
            return $row->position;
        } else {
            return 0;
        }
    }

Next you need to create the methods to move the menu item up and down. Start with the moveUp() method. This method needs to load the menu item. If it does not find the menu item, then throw an exception. Next you need to check the current menu position. If it is already the first item, return false. Otherwise, find the previous item in the menu. Once you find this row, switch the positions, and save both rows (Listing 7-37).

Example 7.37. The moveUp() Method in application/models/MenuItem.php

public function moveUp($itemId)
{
    $row = $this->find($itemId)->current();
    if($row) {
        $position = $row->position;
        if($position < 1) {
               // this is already the first item
               return FALSE;
        }else{
            //find the previous item
            $select = $this->select();
         $select->order('position DESC'),
            $select->where("position < ?", $position);
            $select->where("menu_id = ?", $row->menu_id);
            $previousItem = $this->fetchRow($select);
            if($previousItem) {
                //switch positions with the previous item
                $previousPosition = $previousItem->position;
                $previousItem->position = $position;
                $previousItem->save();
                $row->position = $previousPosition;
                $row->save();
            }
        }
    } else {
        throw new Zend_Exception("Error loading menu item");
    }
}

Next do the moveDown() method. This method will also fetch the current item, as with the moveUp() method. Then it checks to see whether the item is already the last item and returns false if it is. If not, it finds the next item and switches positions with it. Listing 7-38 shows the complete moveDown() method.

Example 7.38. The moveDown() Method in application/models/MenuItem.php

public function moveDown($itemId) {
    $row = $this->find ( $itemId )->current ();
    if ($row) {
        $position = $row->position;
        if ($position == $this->_getLastPosition ( $row->menu_id )) {
            // this is already the last item
            return FALSE;
        } else {
            //find the next item
            $select = $this->select ();
            $select->order ( 'position ASC' );
            $select->where ( "position > ?", $position );
                    $select->where("menu_id = ?", $row->menu_id);
            $nextItem = $this->fetchRow ( $select );
            if ($nextItem) {
                //switch positions with the next item
                $nextPosition = $nextItem->position;
                $nextItem->position = $position;
                $nextItem->save ();
                $row->position = $nextPosition;
                $row->save ();
            }
        }
    } else {
        throw new Zend_Exception ( "Error loading menu item" );
    }
}

Now that you have the model methods to manage the menu item positions, you can create the moveAction() method in the MenuItemModel (Listing 7-39). This action will be relatively simple because it does not require a view or a form. It requires two URL parameters: the ID of the menu item and the direction to move it. Once it fetches these values, it loads the requested menu item, and then it runs either moveUp() or moveDown(), depending on the value of the direction parameter. Finally, it sets the menu parameter (since the index action expects this) and redirects to the indexAction().

Note

Some controller actions just perform an action and then forward to another action to render. In these cases, I often just write the method rather than using Zend_Tool. Zend_Tool will always create the view script, even for forwarding actions.

Example 7.39. The moveAction() in application/controllers/MenuItemController.php

public function moveAction() {
    $id = $this->_request->getParam ( 'id' );
    $direction = $this->_request->getParam ( 'direction' );
$mdlMenuItem = new Model_MenuItem ( );
    $menuItem = $mdlMenuItem->find ( $id )->current ();
    if ($direction == 'up') {
        $mdlMenuItem->moveUp ( $id );
    } elseif ($direction == 'down') {
        $mdlMenuItem->moveDown ( $id );
    }
    $this->_request->setParam ( 'menu', $menuItem->menu_id );
    $this->_forward ( 'index' );
}

Updating Menu Items

The next step is updating the menu items. This will function exactly the same as the updateAction() method in the MenuController.

Loading the Update Item Form

Create the menu item update action using Zend_Tool, as shown in Listing 7-40.

Example 7.40. Creating the Update Menu Item Action Using Zend_Tool

zf create action update menuitem

Then fill in the update action. The menu item ID will be passed to this action as a URL parameter, so get this value. Then load the menu item and its parent menu. Fetch a new instance of the menu item form. Then populate the form with the menu item row, and pass the form to the view. See Listing 7-41 for the completed action.

Example 7.41. Loading the Menu Form in the updateAction() of the MenuItemController in application/controllers/MenuItemController.php

public function updateAction()
{
    $id = $this->_request->getParam ( 'id' );
    // fetch the current item
    $mdlMenuItem = new Model_MenuItem ( );
    $currentMenuItem = $mdlMenuItem->find ( $id )->current ();
    // fetch its menu
    $mdlMenu = new Model_Menu ( );
    $this->view->menu = $mdlMenu->find ( $currentMenuItem->menu_id )->current ();
    // create and populate the form instance
    $frmMenuItem = new Form_MenuItem();
        $frmMenuItem->setAction('/menuitem/update'),
    $frmMenuItem->populate ( $currentMenuItem->toArray () );
    $this->view->form = $frmMenuItem;
}

Rendering the Update Item Form

Now you need to render the update form. Zend_Tool created a view script for the update action in application/views/scripts/menuitem/update.phtml. Open this file in your editor. Render a headline and the current menu name on this page, and then render the form, as shown in Listing 7-42.

Example 7.42. Rendering the Update Page Form in application/views/scripts/menuitem/update.phtml

<h2>Update Menu Item</h2>
<p>Current Menu: <?php echo $this->menu->name;?></p>
<p>To update this menu item fill out the form below and click submit...</p>
<?php echo $this->form; ?>

Processing the Update Item Form

The next step is processing the update form when it is posted back. First, create a new function to update the menu item in the MenuItem model. This method should find the row it needs to update. If it doesn't find the row, then throw a new Zend_Exception. Next update the page label and the page_id. Then you need to check the value of the page_id; if it is less than 1 (not set), then you should use the value of the link, which is what will be used for static links to modules. Note that the menu_id is not one of the parameters in this method; you can't change the menu an item is in. Listing 7-43 shows the completed updateItem() method.

Example 7.43. The updateItem() in application/models/MenuItem.php

public function updateItem($itemId, $label, $pageId = 0, $link = null) {
    $row = $this->find ( $itemId )->current ();
    if ($row) {
        $row->label = $label;
        $row->page_id = $pageId;
        if ($pageId < 1) {
            $row->link = $link;
        } else {
            $row->link = null;
        }
        return $row->save ();
    } else {
        throw new Zend_Exception ( "Error loading menu item" );
    }
}

Now you can update the updateAction() in the MenuItemController to process the form. This will process the form much like the updateAction() did in the MenuController, with one difference. Since you manage menu items based on the menu, then you need to set the menu parameter before forwarding to the indexAction(). See Listing 7-44 for the completed action.

Example 7.44. The Updated updateAction() in application/controllers/MenuitemController.php

public function updateAction ()
{
    $id = $this->_request->getParam('id'),
    // fetch the current item
    $mdlMenuItem = new Model_MenuItem();
    $currentMenuItem = $mdlMenuItem->find($id)->current();
    // fetch its menu
    $mdlMenu = new Model_Menu();
    $this->view->menu = $mdlMenu->find($currentMenuItem->menu_id)->current();
    // create and populate the form instance
    $frmMenuItem = new Form_MenuItem();
    $frmMenuItem->setAction('/menuitem/update'),
    // process the postback
    if ($this->_request->isPost()) {
        if ($frmMenuItem->isValid($_POST)) {
            $data = $frmMenuItem->getValues();
            $mdlMenuItem->updateItem($data['id'], $data['label'],
                $data['page_id'], $data['link']);
            $this->_request->setParam('menu', $data['menu_id']);
            return $this->_forward('index'),
        }
    } else {
        $frmMenuItem->populate($currentMenuItem->toArray());
    }
    $this->view->form = $frmMenuItem;
}

Deleting Menu Items

The final step in managing menus is deleting menu items. This will be very straightforward because it does not require a view, just the deleteAction() method in the MenuItemController and a deleteItem() method in the MenuItem model.

To get started, create the deleteItem() method in the MenuItem model. This method will try to find the menu, and if it is successful, it will delete the menu. If not, it will throw an exception, as shown in Listing 7-45.

Example 7.45. The deleteItem() Method in application/models/MenuItem.php

public function deleteItem($itemId) {
    $row = $this->find ( $itemId )->current ();
    if ($row) {
        return $row->delete ();
    } else {
        throw new Zend_Exception ( "Error loading menu item" );
    }
}

Next create the deleteAction() method in the MenuItemController. This action will create a new instance of the MenuItem model and find the menu item that matches the ID that was passed to the action in the URL parameter. Next it runs the deleteItem() method. Then it sets the menu parameter and forwards to the indexAction() method. Listing 7-46 shows the complete method.

Example 7.46. The deleteAction() Method in application/controllers/MenuItemController.php

public function deleteAction() {
    $id = $this->_request->getParam ( 'id' );
    $mdlMenuItem = new Model_MenuItem ( );
    $currentMenuItem = $mdlMenuItem->find ( $id )->current ();
    $mdlMenuItem->deleteItem ( $id );
    $this->_request->setParam ( 'menu', $currentMenuItem->menu_id );
    $this->_forward ( 'index' );
}

Rendering Menus

Now that the menu management component is complete, you are ready to render the menus. To do this, create a new action in the MenuController named renderAction(). You can do this with Zend_Tool, as shown in Listing 7-47.

Example 7.47. Creating the Render Menu Action with Zend_Tool

zf create action render menu

Zend_Navigation is a new component that has been developed to make managing your site navigation as easy as possible. To use Zend_Navigation, you need to first fetch all items from the requested menu. Then load each of those items into an array. When this is complete, you create a new instance of Zend_Navigation, which you pass the array to. Finally, you pass this to the Zend_View navigation helper, as shown in Listing 7-48.

Example 7.48. The renderAction() Method in application/controllers/MenuController.php

public function renderAction()
{
    $menu = $this->_request->getParam ( 'menu' );
    $mdlMenuItems = new Model_MenuItem ( );
    $menuItems = $mdlMenuItems->getItemsByMenu ( $menu );

    if(count($menuItems) > 0) {
        foreach ($menuItems as $item) {
            $label = $item->label;
            if(!empty($item->link)) {
                $uri = $item->link;
            }else{
                $uri = '/page/open/id/' . $item->page_id;
            }
$itemArray[] = array(
                'label'        => $label,
                'uri'        => $uri
            );
        }
        $container = new Zend_Navigation($itemArray);
        $this->view->navigation()->setContainer($container);
    }
}

Now you need to update the view script that Zend_Tool created. Since you have already loaded the navigation helper, it is ready to render. You call its menu() method to render it as a menu, as shown in Listing 7-49.

Example 7.49. The Render Menu View Script in application/views/scripts/menu/render.phtml

<?php echo $this->navigation()->menu(); ?>

Creating the Main Site Menus

With the menu management component complete, you can create the main site menus. For right now, create two menus: the main menu and the admin menu.

Creating the Main Menus

To create the main menu, point your browser to http://localhost/menu/create, and create a new menu named main_menu. Then click the Manage Menu Items link on the menu list, and add a few items (whatever you want) to this menu. To make managing the CMS easier, you will probably want to create an admin menu. Point your browser to /menu/create, and create a new menu named admin_menu. Then click the Manage Menu Items link on the menu list. Add each of the menu items in Table 7-1.

Table 7.1. The Admin Menu Items

Label

Link

Manage Content

/page

Manage Menus

/menu

Setting the Main Menu GUIDs

You generally do not want to hard-code a GUID into your scripts if you can avoid it, since this is something that can be changed through the CMS. Instead, it is preferable to set the GUIDs for these items in a config file or in the application bootstrap. In this case, use the latter, as you did with the view skin. Create a new method in the Bootstrap.php file named _initMenus(). Fetch the view from the bootstrap and then pass the menu ids to the view, as shown in Listing 7-50.

Example 7.50. The initMenus() Method in application/Bootstrap.php

protected function _initMenus ()
{
    $view = $this->getResource('view'),
    $view->mainMenuId = 1;
    $view->adminMenuId = 2;
}

Rendering the Main Menus

There are already placeholders for the main menu and admin menu in the site layout file. Open application/layouts/scripts/layout.phtml. Now render the main menu using the Zend_View action() helper. The action() helper enables you to call a different controller action from the view; the helper then returns the response that the action renders. Behind the scenes it clones the request so you should always consider this overhead when you use it. I prefer to set these placeholders at the top of the page. This makes it possible to fetch information from them throughout the page (Listing 7-51).

Example 7.51. Rendering the Main Menu in application/layouts/scripts/layout.phtml

$this->layout()->nav = $this->action('render', 'menu', null,
    array('menu' => $this->mainMenuId));

Now you should update the styles for the #nav div to make your menu look more like a menu and less like a list. First create a new CSS file in your blues skin named nav.css. Then add this file to the skin.xml file in the root of the blues skin. Locate the <stylesheet> section, and add the reference to nav.css, as shown in Listing 7-52.

Example 7.52. The nav.css Reference to Add into public/skins/blues/skin.xml

<stylesheet>nav.css</stylesheet>

Next style this menu. Add the CSS from Listing 7-53 into the new nav.css file.

Example 7.53. The Nav Style in public/skins/blues/css/nav.css

@CHARSET "ISO-8859-1";
#nav ul{
    list-style:none;
}

#nav ul li{
    display:inline;
    padding:0 20px;
}

#nav ul li a{
    font-family:"Arial Black";
color:#FCE6C8;
    font-size:16px;
    text-decoration:none;
}

#nav ul li a:hover{
    color:#fff;
}

#nav ul li a.selected{
    font-weight:bold;
}

Rendering the Admin Menu

In the next chapter, you are going to learn about Zend Framework security. You will update this menu to render conditionally depending on the current user's permission. For now, you can just render the menu in the placeholder (Listing 7-54).

Example 7.54. Rendering the Main Menu in application/layouts/scripts/layout.phtml

$this->layout()->adminMenu = $this->action(
    'render', 'menu', null, array('menu' => $this->adminMenuId)
);

Now when you point your browser at http://localhost, you should see both of your menus rendering, as shown in Figure 7-3.

The home page with menus

Figure 7.3. The home page with menus

Creating SEO-Friendly URLs

One final note on navigation is search engine optimization (SEO). Most people are very sensitive about SEO-friendly URLs now, as well they should be. It looks better if nothing else. Zend Framework follows the best practices by default, since it does not rely on long query strings. The way the CMS is set up also makes it fairly easy to introduce simple SEO-friendly URLs after the fact. Keep in mind that it is possible to make these much more attractive, but for now just set the CMS up to use the page title rather than the ID. You will need to update two files to do this: the PageController and the MenuController.

Note

If you do turn on SEO friendly URLs, you should add an index to the pages table on the title column.

Start by updating the MenuController's render action (Listing 7-55).

Example 7.55. The Updated renderAction() Method in application/controllers/MenuController.php

public function renderAction()
{
    $menu = $this->_request->getParam ( 'menu' );
    $mdlMenuItems = new Model_MenuItem ( );
    $menuItems = $mdlMenuItems->getItemsByMenu ( $menu );

    if(count($menuItems) > 0) {
        foreach ($menuItems as $item) {
            $label = $item->label;
            if(!empty($item->link)) {
                $uri = $item->link;
            }else{
                // update this to form more search-engine-friendly URLs
                $page = new CMS_Content_Item_Page($item->page_id);
                $uri = '/page/open/title/' . $page->name;
            }
            $itemArray[] = array(
                'label'        => $label,
                'uri'        => $uri
            );
        }
        $container = new Zend_Navigation($itemArray);
        $this->view->navigation()->setContainer($container);
    }
}

Now that the menu is using the title in the URL, you need to update the page controller's openAction() to fetch the page by the title rather than ID, as shown in Listing 7-56.

Example 7.56. The Updated openAction() in application/controllers/PageController.php

public function openAction()
{
    $title = $this->_request->getParam('title'),
    $id = $this->_request->getParam('id'),
    // first confirm the page exists
    $mdlPage = new Model_Page();
    $select = $mdlPage->select();
    $select->where('name = ?', $title);
    $row = $mdlPage->fetchRow($select);
    if($row) {
$this->view->page = new CMS_Content_Item_Page($row->id);
    }else{
        // the error handler will catch this exception
        throw new Zend_Controller_Action_Exception(
            "The page you requested was not found", 404);
    }
}

Summary

In this chapter, you added navigation management to your CMS project.

You started by creating new menus, which involved creating a form, a model, and a controller for them. Then you updated and deleted the menus. In the next part, you worked with menu items. The menu items' CRUD functionality was virtually identical to that of the menus.

Once this was done, you created a method to load the menu items into the Zend_Navigation component and render them. You then used these tools to create the actual site and admin menus, which you added to the site layout file.

Finally, you updated the menus to use more neatly formed, SEO-friendly URLs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset