Dynamic Menu Builder for Bootstrap 3: Menu Manager

Reza Lavaryan
This entry is part 1 of 2 in the series Dynamic Menu Builder for Bootstrap 3

Dynamic Menu Builder for Bootstrap 3

Creating menus and navigation bars has never been easier than with Twitter Bootstrap. We can easily create stylish navigation bars without too much effort. While it’s enough for some projects, you might encounter situations when you need to have more control over the links and items inside your menu.
Imagine you need to load the items from a database table. What if you need to limit the items to a group of users having the required permissions?
This is where static solutions can’t help much, so, we should think of a more dynamic approach.

In this tutorial I’m going to show you how to create your own dynamic menu builder in PHP. This is a two part series, with the first part focusing on demo code and the Menu class, and the second part taking care of the other classes and usage examples.

Defining The Goal

Before we start, let’s briefly outline what we need to accomplish. Personally I like to get more with writing less and I’m sure you do as well.

We should be able to create menus the easy way, but the code should stay clean and professional, written in modern object oriented style, like so:

//create the menu
$menu = new Menu;

//Add some items
$menu->add('Home', '');
$menu->add('About', 'about');
$menu->add('Services', 'services');
$menu->add('Portfolio', 'portfolio');
$menu->add('contact', 'contact');

We should be able to add sub items in a semantic way rather than explicitly defining a pid for each sub item:

//...
$about = $menu->about('about', 'About');
    $about->add('Who we are?', 'who-we-are');
    $about->add('What we do?', 'what-we-do');
//...

We should provide a way for adding HTML attributes to the items:

//...
$menu->add('About', array('url' => 'about', 'class' => 'about-li active', 'id' => 'about-li'));
//...

Wouldn’t it be fantastic if we could easily append or prepend some content to the anchors like a caret symbol or a graphical icon?

//...
$about = $menu->add('About', array('url' => 'about', 'class' => 'about-li active', 'id' => 'about-li'));
$about->link->append('<b class="caret"></b>')
            ->prepend('<span class="glyphicon glyphicon-user"></span>');
//...

We should also provide a way to filter the items:

$menu = new Menu;

$menu->add('Home', '');
$menu->add('About', 'about');
$menu->add('Services', 'services');
$menu->add('Portfolio', 'portfolio');
$menu->add('contact', 'contact');

$menu->filter( function($item){
    if( statement ){
        return true;
    }
    return false;    
});

Menus should be rendered as HTML entities like lists, divs or any boilerplate code we need:

    //...
    // render the menu as an unordered list
    echo $menu->asUl();
    
    // render menu as an ordered list
    echo $menu->asOl();
    
    // render menu as an ordered list
    echo $menu->asDiv();
    
    //...

Think we can pull it off?

Creating The Menu Builder

I’m going to split our menu builder into three independent components:

  • Menu manager manages the menu items. It creates, modifies and renders the items.
  • Item represents menu items as objects. It stores title, link, attributes and extra data with each item.
  • Link represents links as objects.

We’re going to implement these components in three separate class definitions: Menu, Item and Link.

These are the methods we’re going to create with a short explanation of what they do and what they return:

Menu

  • Item add(string $title, mixed $options) Adds an item.
  • array roots() Returns items at root level (items with no parent).
  • array whereParent(int $parent) Returns items with the given parent id.
  • Menu filter(callable $closure) Filters items by a callable passed by the user.
  • string render(string $type) Renders menu items in HTML format.
  • string getUrl(array $options) Extracts the URL from user $options.
  • array extractAttr(array $options) Extracts valid HTML attributes from user $options.
  • string parseAttr(array $attributes) Generates a string of key=”value” pairs(separated by space).
  • int length() Counts all items in the menu.
  • string asUl(array $attributes) Renders the menu as an unordered list.
  • string asOl(array $attributes) Renders the menu as an ordered list.
  • string asDiv(array $attributes) Renders the menu as HTML
    .

Item

  • Item add(string $title, mixed $options) Adds a sub item.
  • int id() Generates a unique Id for the item.
  • int get_id() Returns item’s Id.
  • int get_pid() Returns item’s pid (parent Id).
  • boolean hasChilderen() Check whether the item has any children or not.
  • array childeren() Returns children of the item.
  • mixed attributes(string $key [, string $value]) Sets or gets attributes of the item.
  • mixed meta(string $key [, string $value]) Sets or gets item’s meta data.

Link

  • string get_url() Returns link URL.
  • string get_text() Returns link text.
  • Link prepend(string $content) Inserts content at the beginning of link text.
  • Link append(string $content) Append content to the link text.
  • mixed attributes(string $key [, string $value]) Sets or gets link’s attributes.

Okay now that we have the prototypes on paper, We can start building them. You can also use the above list as a quick documentation cheat sheet for when you inevitably use our end product in a project.

Run your favorite text editor, create a new file and name it menu.php.

menu.php

class Menu {

	protected $menu = array();
	protected $reserved = array('pid', 'url');
	
	//...
	
}

Menu has two protected attributes:

  • $menu Contains objects of type Item.
  • $reserved Contains reserved options.

$menu stores an array of registered items.

Okay, what about $reserved? What is it exactly?

When the user registers an item, he/she passes an array of options through the add method. Some options are HTML attributes and some just contain the information we need inside the class.

   //...
	$menu->add('About', array('url' = 'about', 'class' => 'item'));
   //...

In this example, class is an HTML attribute but url is a reserved key.
We do this to distinguish HTML attributes from other data.

add(title, options)

This method creates an item:

public function add($title, $options)
{
	$url  = $this->getUrl($options);
	$pid  = ( isset($options['pid']) ) ? $options['pid'] : null;
	$attr = ( is_array($options) ) ? $this->extractAttr($options) : array();
	
	$item = new Item($this, $title, $url, $attr, $pid);
	
	array_push($this->menu, $item);
	
	return $item;
}

add() accepts two parameters, the first one is title and the second one is options.

Options can be a simple string or an associative array of options.
If options is just a string, add() assumes that the user wants to define the URL without any other options.

To create an Item we need to provide the following data:

  • $title item’s title
  • $url item’s link URL
  • $pid item’s pid (if it is a sub item)
  • $attr item’s HTML attributes
  • $manager a reference to the Menu object (Menu manager)

$url is obtained from getUrl() method which we’re going to create later; So let’s just get the URL regardless of how it works.

Next, we’ll check if $options contains key pid, if it does, we store it in $pid.

We also store a reference to the Menu object with each item. This reference allows us to use methods of the menu manager within Item context (we’ll talk about this later).

Lastly, we create the item, push it to $menu array and return the Item.

There we go! we created our first method.

roots()

Fetches items at root level by calling method whereParent(parent) which we’ll create shortly.

public function roots() 
{
	return $this->whereParent();
}

whereParent(parent)

Returns items at the given level.

public function whereParent($parent = null)
{
	return array_filter($this->menu, function($item) use ($parent){
		
		if( $item->get_pid() == $parent ) {
			
			return true;
		}
		
		return false;
	});
}

Inside whereParent() we make use of PHP’s built-in function array_filter on $menu array.

If you call the method with no argument, It will return items with no parent meaning items at root level. That’s why roots() calls whereParent() with no argument!

filter(callback)

Filters items based on a callable provided by the user.

public function filter($closure)
{
	if( is_callable($closure) ) {
		
		$this->menu = array_filter($this->menu, $closure);

	}

	return $this;
}

Isn’t it simple? It just accepts a callable as parameter and runs PHP’s array_filter on $menu array.
closure depends on how you like to limit your menu items. I’m going to show you how to use this later.

render(type, pid)

This method renders the items in HTML format.

public function render($type = 'ul', $pid = null)
{
	$items = '';
	
	$element = ( in_array($type, ['ul', 'ol']) ) ? 'li' : $type;
	
	foreach ($this->whereParent($pid) as $item)
	{
		$items .= "<{$element}{$this->parseAttr($item->attributes())}>";                  $items .= $item->link();

		if( $item->hasChildren() ) {
			
			$items .= "<{$type}>";
			
			$items .= $this->render($type, $item->get_id());
			
			$items .= "</{$type}>";
		}
		
		$items .= "</{$element}>";
	}

	return $items;
}

$element is the HTML element which is going to wrap each item.
You might ask so what’s that $type we pass through render() then?

Before answering this question, let’s see how items are rendered as HTML code:

If we call render() with ul as $type, The output would be:

<ul>
	<li><a href="about">About</a></li>
	<li><a href="services">Services</a></li>
	<li><a href="portfolio">Portfolio</a></li>
	<li><a href="contact">Contact</a></li>
</ul>

Or if we call it with div as $type:

<div>
	<div><a href="about">About</a></div>
	<div><a href="services">Services</a></div>
	<div><a href="portfolio">Portfolio</a></div>
	<div><a href="contact">Contact</a></div>
</div>

Some HTML elements are a group of parent/child tags like lists.
So when I call render('ul'), I’m expecting it to wrap each item in a <li> tag.

	$element = ( in_array($type, ['ul', 'ol']) ) ? 'li' : $type;

This line checks if $type is in the array('ul', 'ol'); If it is, it will return li otherwise it’ll return $type.

Next, we iterate over $menu elements with the given $parent. If $parent is null, it’ll start with the items at root level.

On each iteration, we check if the item has any children. If it does, the method would call itself to render the children and children of children.

Inside our menu builder, we have several methods which act as helpers:

getUrl(options)

Extracts URL from $options.

public function getUrl($options)
{
	if( ! is_array($options) ) {
			return $options;
		} 

		elseif ( isset($options['url']) ) {
			return $options['url'];
		} 

		return null;
}

You might ask why do we need such a function in the first place? Doesn’t the user provide the url when registering the item?

You’re right! But as shown earlier, we have two ways to define the URL:

	...
	// This is the quick way
	$menu->add('About', 'about');
	
	// OR
	
	$menu->add('About', array('url' => 'about'));
	...

getUrl() returns the $options itself if it’s just a simple string and if it’s an array, it looks for key ‘url` and returns the value.

extractAttr(options)

extractAttr gets the $options array and extracts HTML attributes from it.

public function extractAttr($options)
{
	return array_diff_key($options, array_flip($this->reserved));
}

As mentioned earlier, some keys in $options are HTML attributes and some are used by class methods. For example class and id are HTML attributes but url is an option which we use inside the class.

As noted earlier our menu manager has an attribute named $reserved. We store class options like url in $reserved array to distinguish them from HTML attributes.

Inside extractAttr() we used array_diff_key() to exclude reserved options.

array_diff_key() compares the keys from $options against the keys from $reserved and returns the difference.

The result will be an array containing all entries from $options which are not available in $reserved. This is how we exclude reserved keys from valid HTML attributes.

Oh wait a minute, array_diff_keys() uses keys for comparison but $reserved has numeric keys:

protected $reserved = array('url', 'pid');

/*
 $reserved = array(0 => 'url', 1 => 'pid');
*/

You are right keen reader! This is when array_flip() comes to the rescue!

array_flip() Exchanges all keys with their associated values in an array. So $reserved will be changed to this:

[ 'url' => 0, 'pid' => 1 ]

parseAttr(attributes)

Item attributes should be in a property=value format to be used in HTML tags.
parseAttr gets an associative array of attributes and coverts it into a string of property=value pairs.

public function parseAttr($attributes)
{
    $html = array();
    foreach ( $attributes as $key => $value)
    {
        if (is_numeric($key)) {
        	$key = $value;
        }	

	    $element = (!is_null($value)) ? $key . '="' . $value . '"' : null;
    
        if (!is_null($element)) $html[] = $element;
    }
    
    return count($html) > 0 ? ' ' . implode(' ', $html) : '';
}

parseAttr iterates over attributes to generate the string. On each iteration we check if the current element has a numeric key; if it does, we will assume that the key and the value are the same.

For example:

  ['class' => 'navbar item active', 'id' => 'navbar', 'data-show']

Would be converted to:

class="navbar item active" id="navbar" data-show="data-show"

The method pushes each pair to an array, then we implode them with a space and return the result to the caller.

length()

Counts all items registered:

public function length() 
{
	return count($this->menu);
}

asUl(attribute)

asUl calls render() and put the result in a <ul> tag.
It also receives an array of attributes in case you need to add some HTML attributes to <ul> itself.

public function asUl($attributes = array())
{
	return "<ul{$this->parseAttr($attributes)}>{$this->render('ul')}</ul>";
}

The other two methods work just like asUl():

asOl(attributes)

asOl calls render() and put the result in a <ol> tag.
It also receives an array of attributes in case you need to add some HTML attributes to <ol> itself.

public function asOl($attributes = array())
{
	return "<ol{$this->parseAttr($attributes)}>{$this->render('ol')}</ol>";
}

asDiv(attributes)

asDiv calls render() and put the result in a <div> tag.
It also gets an array of attributes in case you need to add som HTML attributes to <div> itself.

public function asDiv($attributes = array())
{
	return "<div{$this->parseAttr($attributes)}>{$this->render('div')}</div>";
}

Wrapping up

In this part, we built our menu manager – the main class for building the menu, which contains all instances of the classes we’ll be building next. You can see the complete source code for all classes here or wait for part 2 which is coming out tomorrow.

Dynamic Menu Builder for Bootstrap 3

Dynamic Menu Builder for Bootstrap 3: Item and Link >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Eduardo WB

    I was looking for something like that! Thank you. I hope to see some examples with Laravel, if possible.

  • Mike Mx Kowalski

    No unit tests, shame.

    • Roy

      A nice way to waste time on such simple code

  • Taylor Ren

    Not very comfortable when mixing the presentation codes with pure logical codes…

    • Reza Lavaryan

      If you mean render, asUl, asOl and asDiv methods, these are just a few helpers included in the class to do some quick tasks. It’s the developer’s decision to isolate them from application logic when calling them.

      These sort of functions are common in popular frameworks out there including Laravel.

      Please note that this helpers aren’t used in advanced use cases; You’re totally free to use the template engine of your choice as explained in the article.

      Good luck.

      • sebastiaan hilbers

        Is part 2 coming today? :)

  • hitasoft

    Thank you for offering

  • alex

    comment were broken. code is on http://pastebin.com/4jmJtynr

  • Reza Lavaryan

    Purpose was making a reusable code providing you more control over you items. A code which is more readable and maintainable!

    • Roy

      How on earth is this more readable than a simple foreach loop?

      • Reza Lavaryan

        Imagine we need to implement this menu:

        —o About
        ——o Departments
        ————o Accounting
        ————o Media

        I think implementing a multilevel menu with an associative array and Simple foreach loop is going to be a little confusing:

        array( “title” => “About”, “href” => “link1″, “sel” => false, “children” => array(“title” => “Departments”, “href” => “link3″, “Children” => array(array( “title” => “Accounting”, “href” => “link4″), array( “title” => “Media”, “href” => “link4″))));

        And how is it possible to render the items to the deepest level using a Simple foreach()?

        By using a class based approach you’ll be able to:

        $menu = new Menu;
        $about = $menu->add(‘About’, ‘link1′);
        $deps = $about->add(‘Departments’, ‘link2′);

        $deps->add(‘Accounting’, ‘link3′);
        $deps->add(‘Media’, ‘link4′);

        Please note this was the simplest use case.

        Apart from all, by using a class-based approach, you’ll be able to modify you items from anywhere in your code.

        Good luck.

        • Roy

          Structured indentation for the arrays would maintain readability. It depends on use case, but not everything must be OO and for simpler menus I really think it is over-engineered. This is useful, but only for more complex menus. For anything else it’s overkill.

          • Reza Lavaryan

            I agree. However indentation wouldn’t help much.
            I wouldn’t recommend this for a simple website menu as noted at the beginning of the article. This is going to save you time when you need to have more control over your items as in a web applications with different levels of accessibility or when you menu is really big.

  • Mohd. Mahabubul ALam

    Creating menus and navigation bars has never been easier than with
    Twitter Bootstrap. We can easily create stylish navigation bars without
    too much effort. While it’s enough for some projects, you might
    encounter situations when you need to have more control over the links
    and items inside your menu.