Navigation extension

Navigation.php




<?php


class Navigation

{

	private static $_instance = null;

	private $_menus = array();

	

	public static function getInstance()

	{

		if(self::$_instance === null)

			self::$_instance = new self;

		return self::$_instance;

	}

	

	public static function getMenu($menuId)

	{

		if(!isset(self::getInstance()->_menus[$menuId]))

			self::getInstance()->_menus[$menuId] = new Menu($menuId);

		return self::getInstance()->_menus[$menuId];

	}

	

	public static function removeMenu($menuId)

	{

		self::getInstance()->_menus[$menuId] = null;

	}

	

	public static function render($menuId)

	{

		if(($menu = self::getMenu($menuId)) !== null)

			$menu->render();

	}

}



Menu.php




<?php


class Menu

{

	private $_id;

	private $_items = array();

	private $_htmlOptions = array();

	

	public function __construct($id)

	{

		$this->_id = $id;

	}

	

	public function addItem($itemId, $item)

	{

		$label = isset($item['label']) ? $item['label'] : '';

		$url= isset($item['url']) ? $item['url'] : '';  

		$weight= isset($item['weight']) ? $item['weight'] : 0; 

		$htmlOptions = isset($item['htmlOptions']) ? $item['htmlOptions'] : array(); 

		$linkOptions = isset($item['linkOptions']) ? $item['linkOptions'] : array(); 

		$visible = isset($item['visible']) ? $item['visible'] : true; 

		$items = isset($item['items']) ? $item['items'] : null; 

		

		$this->_items[$itemId] = new MenuItem($itemId,$label,$url,$weight,$htmlOptions,$linkOptions,$visible,$items);

	}

	

	public function addItems($items)

	{

		foreach($items as $id => $item)

			$this->addItem($id, $item);

	}

	

	public function removeItem($itemId)

	{

		if(isset($this->_items[$itemId]))

			unset($this->_items[$itemId]);

	}

	

	public function hasItem($itemId)

	{

		return isset($this->_items[$itemId]);

	}

	

	public function isEmpty()

	{

		$count = 0;

		foreach($this->_items as $item)

		{

			if($item->getVisible())

				$count++;

		}

		return $count <= 0;

	}

	

	public function getItem($itemId)

	{

		return isset($this->_items[$itemId]) ? $this->_items[$itemId] : null;

	}

	

	public function setHtmlOptions($htmlOptions = array(), $mergeOld = false)

	{

		if($mergeOld)

			$this->_htmlOptions = CMap::mergeArray($this->_htmlOptions, $htmlOptions);

		else

			$this->_htmlOptions = $htmlOptions;

		return $this;

	}

	

	public function getVisibleItems()

	{

		$visibleItems = array();

		foreach($this->_items as $id => $item)

		{

			if($item->getVisible())

				$visibleItems[$id] = $item;

		}

		return $visibleItems;

	}

	

	protected function compareItems($itemA, $itemB)

	{

		if($itemA->getWeight() === $itemB->getWeight())

		{

			return 0;

		}

		return ($itemA->getWeight() <= $itemB->getWeight()) ? -1 : 1;

	}

	

	protected function sortItems($items)

	{

		uasort($items,array($this,'compareItems'));

		return $items;

	}

	

	public function render()

	{

		if(count($this->getVisibleItems()))

		{

			echo CHtml::openTag('ul', $this->_htmlOptions)."\n";

			$this->renderRecursive($this->getVisibleItems());

			echo CHtml::closeTag('ul')."\n";

		}

	}

	

	protected function renderRecursive($items)

	{

		$items = $this->sortItems($items);

		

		foreach($items as $item)

		{

			if($item instanceof MenuItem)

			{

				if($item->getVisible())

				{

					echo CHtml::openTag('li',$item->getHtmlOptions())."\n";

					echo CHtml::link($item->getLabel(), $item->getUrl(), $item->getLinkOptions());

					if(count($item->getItems()))

					{

						echo CHtml::openTag('ul')."\n";

						self::renderRecursive($item->getItems());

						echo CHtml::closeTag('ul')."\n";

					}

					echo CHtml::closeTag('li')."\n";

				}

			}

		}

	}

}



MenuItem.php




<?php


class MenuItem

{

	private $_id;

	private $_label;

	private $_url;

	private $_weight;

	private $_htmlOptions;

	private $_linkOptions;

	private $_visible;

	private $_items = array();

	

	public function __construct($id, $label, $url, $weight = 0, $htmlOptions = array(), $linkOptions = array(), $visible = true, $items = array())

	{

		$this->_id = $id;

		$this->_label = $label;

		$this->_url = $url;

		$this->_weight = $weight;

		$this->_htmlOptions = $htmlOptions;

		$this->_linkOptions = $linkOptions;

		$this->_visible = $visible;

		$this->_items = $items;

	}

	

	public function getLabel()

	{

		return $this->_label;

	}

	

	public function getUrl()

	{

		return $this->_url;

	}

	

	public function getWeight()

	{

		return $this->_weight;

	}

	

	public function getHtmlOptions()

	{

		return $this->_htmlOptions;

	}

	

	public function setHtmlOptions($htmlOptions = array(), $mergeOld = false)

	{

		if($mergeOld)

			$this->_htmlOptions = CMap::mergeArray($this->_htmlOptions, $htmlOptions);

		else

			$this->_htmlOptions = $htmlOptions;

		return $this;

	}

	

	public function setLinkOptions($linkOptions = array(), $mergeOld = false)

	{

		if($mergeOld)

			$this->_linkOptions = CMap::mergeArray($this->_linkOptions, $linkOptions);

		else

			$this->_linkOptions = $linkOptions;

		return $this;

	}

	

	public function getLinkOptions()

	{

		return $this->_linkOptions;

	}

	

	public function getVisible()

	{

		return $this->_visible;

	}

	

	public function getItems()

	{

		return $this->_items;

	}

	

	public function getItem($id)

	{

		return isset($this->_items[$id]) ? $this->_items[$id] : null;

	}

	

	public function addItem($id, $item)

	{

		$label = isset($item['label']) ? $item['label'] : '';

		$url= isset($item['url']) ? $item['url'] : '';  

		$weight= isset($item['weight']) ? $item['weight'] : 0; 

		$htmlOptions = isset($item['htmlOptions']) ? $item['htmlOptions'] : array(); 

		$linkOptions = isset($item['linkOptions']) ? $item['linkOptions'] : array(); 

		$visible = isset($item['visible']) ? $item['visible'] : true; 

		$items = isset($item['items']) ? $item['items'] : null; 

		

		$this->_items[$id] = new self($id,$label,$url,$weight,$htmlOptions,$linkOptions,$visible,$items);

	}

	

	public function addItemIfHasItem($itemId, $id, $item)

	{

		if($this->getItem($itemId)!==null)

			$this->getItem($itemId)->addItem($id, $item);

	}

	

	public function addItems($items)

	{

		foreach($items as $id => $item)

		{

			$this->addItem($id, $item);

		}

	}

	

	public function removeItem($id)

	{

		if(isset($this->_items[$id]))

			unset($this->_items[$id]);

	}

}



Usage example: building menu




		Navigation::getMenu('main.nav')->setHtmlOptions(array('class'=>'test'))->addItems(array(

			'login'=>array('label'=>'h', 'url'=>'h','weight'=>1,'htmlOptions'=>array('class'=>'l')),

			'login2'=>array('label'=>'l', 'url'=>'l','weight'=>0,'linkOptions'=>array('class'=>'l')),

		));

		

		Navigation::getMenu('user.nav')->addItems(array(

			'login'=>array('label'=>'xxxxx', 'url'=>'xxx'),

			'login2'=>array('label'=>'yyyy', 'url'=>'xxx'),

		));

		Navigation::getMenu('user.nav')->getItem('login')->addItems(array(

			'login'=>array('label'=>'wwww', 'url'=>'xxx', 'visible'=>false),

			'login2'=>array('label'=>'mmmm', 'url'=>'xxx'),

		));

		Navigation::getMenu('user.nav')->getItem('login')->getItem('login2')->addItems(array(

			'login'=>array('label'=>'nnn', 'url'=>'xxx','weight'=>0),

			'login2'=>array('label'=>'ooo', 'url'=>'xxx','weight'=>0),

		));



Example: rendering menu




		<div id="global-nav clearfix">

			<div id="main-nav">

				<?php Navigation::render('main.nav'); ?>

			</div>

			<div id="user-nav">

				<?php Navigation::render('user.nav'); ?>

			</div>

		</div>



Feel free to suggest changes, improve and share changes :)

What’s with the memoization and instances?

Where did you plan on placing this in your code structure?

Cheers!

Just figured out why, when I tried to remove them and use a single instantiation. =)

But I’m still curious where you place the menu building code.

I place it in the init() method of certain controllers. You can place it anywhere really as you are able to to add or remove items from any menu by calling addItem/removeItem

Updated. Documentation and examples coming soon




<?php


class Menu extends CComponent

{

	protected $_id;

	protected $_items;

	protected $_htmlOptions = array();

	

	public function __construct($id, $htmlOptions = array())

	{

		$this->setId($id);

		$this->setHtmlOptions($htmlOptions);

		$this->_items = new CMap;

		$this->init();

	}

	

	public function init()

	{

	}

	

	public function getId()

	{

		return $this->_id;

	}

	

	public function getHtmlOptions()

	{

		return $this->_htmlOptions;

	}

	

	public function setId($id)

	{

		if(!is_string($id))

		{

			throw new CException(Util::t('The id must be a string.'));

		}

		$this->_id = $id;

		return $this;

	}

	

	public function setHtmlOptions($htmlOptions, $mergeOld = false)

	{

		if(!is_array($htmlOptions))

		{

			throw new CException(Util::t('The html options must be a number.'));

		}

		if($mergeOld)

		{

			$this->_htmlOptions = CMap::mergeArray($this->_htmlOptions, $htmlOptions);

		}else{

			$this->_htmlOptions = $htmlOptions;

		}

		return $this;

	}

	

	public function addItem($itemData)

	{

		if(isset($itemData['id'], $itemData['label']))

		{

			$id          = $itemData['id'];

			$label       = $itemData['label'];

			$url         = isset($itemData['url'])         ? $itemData['url'] : '#';

			$weight      = isset($itemData['weight'])      ? $itemData['weight'] : 0;

			$htmlOptions = isset($itemData['htmlOptions']) ? $itemData['htmlOptions'] : array();

			$linkOptions = isset($itemData['linkOptions']) ? $itemData['linkOptions'] : array();

			$visible     = isset($itemData['visible'])     ? $itemData['visible'] : true;

			

			$this->_items->add($id, new MenuItem($id, $label, $url, $weight, $htmlOptions, $linkOptions, $visible));

		}else{

			throw new CException(Util::t('The item id and label must at least be specified.'));

		}

	}

	

	public function addItems($items)

	{

		foreach($items as $itemData)

		{

			$this->addItem($itemData);

		}

	}

	

	public function removeItem($itemId)

	{

		if($this->_items->contains($itemId))

		{

			$this->_items->remove($itemId);

		}

	}

	

	public function getItems()

	{

		return $this->_items;

	}

	

	public function getItem($itemId)

	{

		return $this->_items->contains($itemId) ? $this->_items[$itemId] : null;

	}

	

	public function hasItem($itemId)

	{

		return $this->getItem($itemId) !== null;

	}

	

	public function getIsEmpty()

	{

		$count = 0;

		foreach($this->_items as $item)

		{

			if($item->getVisible())

			{

				$count++;

			}

		}

		return $count === 0;

	}

	

	public function getVisibleItems()

	{

		$visibleItems = new CMap;

		

		foreach($this->getItems() as $id => $item)

		{

			if($item->getVisible()===true)

			{

				$visibleItems->add($id, $item);

			}

		}

		return $visibleItems;

	}

	

	protected function compareItems($itemA, $itemB)

	{

		if($itemA instanceof MenuItem && $itemB instanceof MenuItem)

		{

			if($itemA->getWeight() === $itemB->getWeight())

			{

				return 0;

			}

			return ($itemA->getWeight() <= $itemB->getWeight()) ? -1 : 1;

		}else{

			throw new CException(Util::t('Both items must be instances of MenuItem or one of it\'s children.'));

		}

	}

	

	protected function sortItems($items)

	{

		$items = $items->toArray();

		

		uasort($items,array($this,'compareItems'));

		

		return new CMap($items);

	}

	

	public function render($return = false)

	{

		$menuContent = '';

		

		if($this->getVisibleItems()->count())

		{

			$menuContent .= CHtml::openTag('ul', $this->_htmlOptions)."\n";

			$menuContent .= $this->renderRecursive($this->getVisibleItems(), true);

			$menuContent .= CHtml::closeTag('ul')."\n";

		}

		if($return)

		{

			return $menuContent;

		}else{

			echo $menuContent;

		}

	}

	

	protected function renderRecursive($items, $return = false)

	{

		$items = $this->sortItems($items);

		

		$content = '';

		

		foreach($items as $item)

		{

			if($item instanceof MenuItem)

			{

				if($item->getVisible())

				{

					$content .= CHtml::openTag('li',$item->getHtmlOptions())."\n";

					$content .= CHtml::link($item->getLabel(), $item->getUrl(), $item->getLinkOptions());

					if(count($item->getVisibleItems()))

					{

						$content .= CHtml::openTag('ul')."\n";

						$content .= self::renderRecursive($item->getVisibleItems(), $return);

						$content .= CHtml::closeTag('ul')."\n";

					}

					$content .= CHtml::closeTag('li')."\n";

				}

			}

		}

		

		if($return)

		{

			return $content;

		}else{

			echo $content;

		}

	}

}


class MenuItem extends Menu

{

	protected $_label;

	protected $_url;

	protected $_weight;

	protected $_linkOptions = array();

	protected $_visible;

	

	public function __construct($id, $label, $url = '#', $weight = 0, $htmlOptions = array(), $linkOptions = array(), $visible = true)

	{

		$this->setId($id);

		$this->setLabel($label);

		$this->setUrl($url);

		$this->setWeight($weight);

		$this->setHtmlOptions($htmlOptions);

		$this->setLinkOptions($linkOptions);

		$this->setVisible($visible);

		

		$this->_items = new CMap;

		

		$this->init();

	}

	

	public function init()

	{

		parent::init();

	}

	

	public function getLabel()

	{

		return $this->_label;

	}

	

	public function getUrl()

	{

		return $this->_url;

	}

	

	public function getWeight()

	{

		return $this->_weight;

	}

	

	public function getLinkOptions()

	{

		return $this->_linkOptions;

	}

	

	public function getVisible()

	{

		return $this->_visible;

	}

	

	public function setLabel($label)

	{

		if(!is_string($label))

		{

			throw new CException(Util::t('The item label must be a string.'));

		}

		$this->_label = $label;

		return $this;

	}

	

	public function setUrl($url)

	{

		if(!(is_array($url) || is_string($url)))

		{

			throw new CException(Util::t('The item url must be a string or an array.'));

		}

		$this->_url = $url;

		return $this;

	}

	

	public function setWeight($weight)

	{

		if(!is_numeric($weight))

		{

			throw new CException(Util::t('The item weight must be a number.'));

		}

		$this->_weight = (int)$weight;

		return $this;

	}

	

	public function setLinkOptions($linkOptions, $mergeOld = false)

	{

		if(!is_array($linkOptions))

		{

			throw new CException(Util::t('The item link options must be a number.'));

		}

		if($mergeOld)

		{

			$this->_linkOptions = CMap::mergeArray($this->_linkOptions, $linkOptions);

		}else{

			$this->_linkOptions = $linkOptions;

		}

		return $this;

	}

	

	public function setVisible($visible)

	{

		if(!is_bool($visible))

		{

			throw new CException(Util::t('The item visibility must be set to a boolean value.'));

		}

		$this->_visible = $visible;

		return $this;

	}

}


class Navigation

{

	private static $_instance;

	

	private $_menus = array();

	

	public function __construct()

	{

		$this->_menus = new CMap;

	}

	

	public static function getInstance()

	{

		if(self::$_instance === null)

		{

			self::$_instance = new self;

		}

		return self::$_instance;

	}

	

	public static function getMenu($menuId)

	{

		$instance = self::getInstance();

		

		if(!$instance->_menus->contains($menuId))

		{

			$instance->_menus[$menuId] = new Menu($menuId);

		}

		return $instance->_menus[$menuId];

	}

	

	public static function removeMenu($menuId)

	{

		$instance = self::getInstance();

		if($instance->_menus->contains($menuId))

		{

			$instance->_menus->remove($menuId);

		}

	}

	

	public static function render($menuId, $return)

	{

		$menuData = '';

		if(($menu = self::getMenu($menuId)) !== null)

		{

			if(count($menu))

			{

				$menuData = $menu->render(true);

			}

		}

		if($return)

		{

			return $menuData;

		}else{

			echo $menuData;

		}

	}

}



Why not Create an Extension or Something to share and improve?

How do you manage various Menu Items?

Do you have any Database Table to manage Menu Items?