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’spid
(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.
Menu
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 typeItem
.$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’spid
(if it is a sub item)$attr
item’s HTML attributes$manager
a reference to theMenu
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.