Automatically Adding items to a Joomla Menu

This example shows how to link a components articles to a Joomla menu using a plugin, aimed at developers, is for a custom component built for Joomla 1.7 and for clarity has no error checking, it is also for a single level menu with its own menu module so that the menu of all items shows for all the the other items in the same menu module.  Although the modified pre-order tree traversal behaviour may appear daunting Wikipedia Tree traversal and Wikipedia Balanced tree among others have clear definitions and good examples to help illuminate and clarify this subject.

Making the task easier and more manageable is by putting the functionality to link an item to a Joomla Menu into a plugin.  There is no additional work required from the model if it extends the JModelAdmin class as these automatically raise the onContentAfterSave and onContentBeforeDelete events.

Our example component is a content plugin called demo2menu and the following config xml file shows the parameters/fields used by the plugin.

<?xml version="1.0" encoding="utf-8" ?> 
<install version="1.6" type="plugin" group="content">
    <name>plg_content_demo2menu</name>
    <author>Persistent Objects</author>
    <creationDate>October 2011</creationDate>
    <copyright>(C) 2011 Persistent Objects Ltd. All rights reserved.</copyright>
    <license>GNU/GPL</license>
    <authorEmail>ahicks@p-o.co.uk</authorEmail>
    <authorUrl>p-o.co.uk</authorUrl>
    <version>1.0</version>
    <description>Adds demo to menu</description>
    <files>
        <filename plugin="demo2menu">demo2menu.php</filename>
        <filename>index.html</filename>
    </files>
    <languages>
        <language tag="en-GB">en-GB.plg_content_demo2menu.ini</language>
        <language tag="en-GB">en-GB.plg_content_demo2menu.sys.ini</language>
    </languages>
    <config>
        <fields name="params">
            <fieldset
 name="plg_content_demo2menu"
 label="Demo content to menu"
            >
                <field
 name="link_menu"
 type="radio"
 label="Link content to menu"
 description="Link content from com_example to menu"
 default="0"
                >
                    <option value="0">NO</option>
                    <option value="1">YES</option>
                </field>
                <field name="menutype" type="sql"
 multiple="false" size="5"
 label="Menu"
 description="Menu to link to"
 query="SELECT menutype, title FROM #__menu_types ORDER BY title ASC"
 key_field="menutype" value_field="title"
                >
                    <option value="">Select menu</option>
                </field>
                <field
 name="link_module"
 type="radio"
 label="Module"
 description="Module Assignment when linking only on the selected pages"
 default="0"
                >
                    <option value="0">NO</option>
                    <option value="1">YES</option>
                </field>
                <field name="module" type="sql"
 multiple="false" size="5"
 label="Module"
 description="Module for Assignment"
 query="SELECT id, title FROM #__modules WHERE module = 'mod_menu' ORDER BY title ASC"
 key_field="id" value_field="title"
                >
                    <option value="">Select module</option>
                </field>
            </fieldset>
        </fields>
    </config>
</install>

The essence is straightforward, when an event is raised, check that you are interested (context) then add, edit or delete the menu item.  The two events are onContentAfterSave so that on a successful item save a menu item can be added or amended, and onContentBeforeDelete so that the menu can be deleted.  If module assignments are used these can also be linked.

<?php
/**
 * Plugin to manage linking demo to menu
 *
 * PHP version 5
 *
 * @categoryPlugin
 * @packagePlgContentDemo2menu
 * @author Alan Hicks <ahicks@p-o.co.uk>
 * @copyright 2010-2011 Persistent Objects Ltd
 * @license Persistent Objects BSD License http://p-o.co.uk/
 *
 * @linkhttp://p-o.co.uk/
 */

// no direct access
defined('_JEXEC') or die('Restricted access');

// Import library dependencies
jimport('joomla.plugin.plugin');

/**
* Plugin class to support adding menu item for com_dexample demo
*
* @categoryPlugin
* @packagePlgContentDemo2menu
* @author Alan Hicks <ahicks@p-o.co.uk>
* @copyright 2010-2011 Persistent Objects Ltd
* @license Persistent Objects BSD License http://p-o.co.uk/
* @linkhttp://p-o.co.uk/
**/
class PlgContentDemo2menu extends JPlugin
{
    /**
 * Database
 */
    private $_db;

    /**
 * Plugin that adds a menu linking to the specified content
 *
 * @paramstring $context The context of the content being passed to the plugin.
 * @paramobject &$args A Table object
 *
 * @return boolean True on success.
 */
    public function onContentAfterSave($context, &$args)
    {
        if ($context != 'com_example.demo') {
            return true;
        }

        if (!($this->params->get('link_menu', '0') ==  1)) {
            return true; // Module enabled but not to update menu!
        }

        if (empty($this->_db)) {
            $this->_db = JFactory::getDBO();
        }

        $menu_id = $this->menuGetId($args->id);

        if ($menu_id) {
            $this->menuUpdate($menu_id, $args);
        } else {
            $menu_id = $this->menuAdd($args);
            
            if ($this->params->get('link_module', '0') ==  1 && $menu_id) {
                $this->menuAddModule($this->params->get('module', 0), $menu_id);
            }
        }

        return true;
    }

    /**
 * Method to add a new or update an existing menu item
 * Updates are limited to Title, Alias, Path and Published
 *
 * @paramint $id Id of the menu to update
 * @paramobject &$args A Table object
 *
 * @return boolean True on success.
 */
    protected function menuUpdate( $id, &$args)
    {

        $menutype = $this->params->get('menutype', '');

        $query = $this->_db->getQuery(true);
        $query->update(`#__menu`);
        $query->set(`title = ` . $this->_db->quote($args->title));
        $query->set(`alias = ` . $this->_db->quote($args->alias));
        $query->set(`published = ` . $this->_db->quote($args->published));
        $query->where(`id = ` . $this->_db->quote($id));

        $success = $this->_runQuery($query, `Unable to update database`);

        return $success;
    }

    /**
 * Method to add a menu item
 *
 * @paramobject &$args A Table object
 *
 * @return int Menu id on success or zero on failure
 */
    protected function menuAdd(&$args)
    {
        $menutype = $this->params->get(`menutype`, ``);

        $query = $this->_db->getQuery(true);
        $query->select(`max(lft) AS max_lft, max(rgt) AS max_rgt`);
        $query->from(`#__menu`);
        $query->where(`menutype = ` . $this->_db->quote($menutype));

        $this->_db->setQuery($query);
        $result = $this->_db->loadObject();

        $query = $this->_db->getQuery(true);
        $query->select(`id, lft, rgt, level`);
        $query->from(`#__menu`);
        $query->where(`lft = ` . $this->_db->quote($result->max_lft));
        $query->where(`rgt = ` . $this->_db->quote($result->max_rgt));

        $this->_db->setQuery($query);
        $result = $this->_db->loadObject();

        // Create space in the tree at the new location for the new node in left ids
        $query = $this->_db->getQuery(true);
        $query->update(`#__menu`);
        $query->set(`lft = lft + 2`);
        $query->where(`lft > ` . $result->lft);
        $success = $this->_db->query();

        // Create space in the tree at the new location
        // for the new node in right ids.
        $query = $this->_db->getQuery(true);
        $query->update(`#__menu`);
        $query->set(`rgt = rgt + 2`);
        $query->where(`rgt > ` . $result->rgt);
        $success = $this->_db->query();

        $query = $this->_db->getQuery(true);
        $query->select(`extension_id`);
        $query->from(`#__extensions`);
        $query->where(`type = ` . $this->_db->quote('component'));
        $query->where(`element = ` . $this->_db->quote('com_example'));

        $this->_db->setQuery($query);
        $extension = $this->_db->loadObject();

        $params = JComponentHelper::getParams(`com_example`);

        // Insert new menu item
        $new_menu_item = new stdClass();

        $new_menu_item->id = 0;
        $new_menu_item->parent_id = 1;
        $new_menu_item->level = 1;
        $new_menu_item->menutype = $menutype;
        $new_menu_item->title = $args->title;
        $new_menu_item->alias = $args->alias;
        $new_menu_item->note = '';
        $new_menu_item->path = $args->alias;
        $new_menu_item->link = `index.php?com_example&view=demo&id=`. $args->id;
        $new_menu_item->type = `component`;
        $new_menu_item->published = $args->published;
        $new_menu_item->level = 1;
        $new_menu_item->component_id = $extension->extension_id;
        $new_menu_item->ordering = 0;
        $new_menu_item->checked_out = 0;
        $new_menu_item->checked_out_time = `0000-00-00 00:00:00`;
        $new_menu_item->browserNav = 0;
        $new_menu_item->access = 1;
        $new_menu_item->img = ``;
        $new_menu_item->template_style_id = 0;
        $new_menu_item->params = $params;
        $new_menu_item->lft = $result->lft + 2;
        $new_menu_item->rgt = $result->rgt + 2;
        $new_menu_item->home = 0;
        $new_menu_item->language = `*`;
        $new_menu_item->client_id = 0;

        $success = $this->_db->insertObject(`#__menu`, $new_menu_item, `id`));

        return $new_menu_item->id;
    }

    /**
 * Method to delete a menu item
 *
 * @paramint $menu_id Menu id to be deleted
 *
 * @return bool True on success or false on failure
 */
    protected function menuDelete($menu_id)
    {
        // Get the menu to be deleted
        $query = $this->_db->getQuery(true);
        $query->select(`lft, rgt`);
        $query->from(`#__menu`);
        $query->where(`id = ` . $this->_db->quote($menu_id));
        $this->_db->setQuery($query);
        $result = $this->_db->loadObject();

        // Delete the menu item
        $query = $this->_db->getQuery(true);
        $query->delete();
        $query->from(`#__menu`);
        $query->where(`id = ` . $this->_db->quote($menu_id));
        $this->_runQuery($query, `Unable to remove menu`);

        // Fill in the space in the tree for lft
        $query = $this->_db->getQuery(true);
        $query->update(`#__menu`);
        $query->set(`lft = lft - 2`);
        $query->where(`lft > ` . $result->lft);
        $this->_runQuery($query, `Shuffle menu lft after delete failed`);

        // Fill in the space in the tree for rgt
        $query = $this->_db->getQuery(true);
        $query->update(`#__menu`);
        $query->set(`rgt = rgt - 2`);
        $query->where(`rgt > ` . $result->rgt);
        $this->_runQuery($query, `Shuffle menu rgt after delete failed`);

        return true;
    }

    /**
 * Method to add a menu item
 *
 * @paramint $moduleid Module id
 * @paramint $menuid Menu id
 *
 * @return bool True on success or false on failure
 */
    protected function menuAddModule($moduleid, $menuid)
    {
        // Insert new menu item
        $menumodule = new stdClass();

        $menumodule->moduleid = $moduleid;
        $menumodule->menuid = $menuid;

        $success = $this->_db->insertObject(`#__modules_menu`, $menumodule);

        return $success;
    }

    /**
 * Method to delete a module menu item
 *
 * @paramint $moduleid Module id
 * @paramint $menuid Menu id
 *
 * @return bool True on success or false on failure
 */
    protected function menuDeleteModule($moduleid, $menuid)
    {
        $query = $this->_db->getQuery(true);
        $query->delete();
        $query->from(`#__modules_menu`);
        $query->where(`moduleid = ` . $this->_db->quote($moduleid));
        $query->where(`menuid = ` . $this->_db->quote($menuid));

        $this->_runQuery($query, `Unable to remove modules menu link`);

        return $success;
    }

    /**
 * Method to get a menu id from the link to the demo item
 *
 * @paramint $id The demo id
 *
 * @return int The Menu ID or Zero if it doesn't exist
 */
    protected function menuGetId( $id = 0 )
    {
        $menutype = $this->params->get(`menutype`, ``);

        $query = $this->_db->getQuery(true);
        $query->select(`id`);
        $query->from(`#__menu`);
        $query->where(`menutype = `.$this->_db->quote($menutype));
        $query->where(`link=`.$this->_db->quote(`index.phhp?com_example&view=demo&id=`.$id);

        $this->_db->setQuery($query);
        $result = $this->_db->loadObject();

        if ($result) {
            return $result->id;
        }

        return 0;
    }

    /**
 * Event that deletes a menu linking to the specified content
 *
 * @paramstring $context The context of the content being passed to the plugin.
 * @paramint $demo A demo object to identify the menu to be deleted
 *
 * @return boolean True on success.
 */
    public function onContentBeforeDelete($context, $demo)
    {
        if ($context != `com_example.demo`) {
            return true;
        }

        if (!($this->params->get(`link_menu`, `0`) ==  1)) {
            return true; // Module enabled but not to update menu!
        }

        if (empty($this->_db)) {
            $this->_db = JFactory::getDBO();
        }

        $menu_id = $this->menuGetId($demo->id);

        if ($menu_id) {
            if ($this->params->get(`link_module`, `0`) ==  1) {
                $this->menuDeleteModule($this->params->get(`module`, 0), $menu_id);
            }
            $this->menuDelete($menu_id);
        }

        return true;
        
    }

    /**
 * Method to run an update query and check for a database error
 *
 * @paramstring $query SQL Query to run
 * @paramstring $errorMessage Error message to raise on failure
 *
 * @return boolean False on exception
 */
    private function _runQuery($query, $errorMessage)
    {
        $this->_db->setQuery($query);
        return true;
    }
}

See Creating a plugin for a primer on creating a plugin, the source of JModelAdmin to see how events are triggered, and thus why subclassing (or extending) is so useful in that we have not needed to amend our model.  JTableNested in libraries/joomla/database/tablenested.php is useful reading for details on how managing menu items works.

By Alan Hicks