Let's face it, creating menus sucks. Menus - a common aspect of any site - can range from being simple and mundane to giant monsters that become a headache to code and maintain.
This bundle solves the issue by giving you a small, yet powerful and flexible framework for handling your menus. While most of the examples shown here are simple, the menus can grow arbitrarily large and deep.
The menu framework centers around one main interface: Knp\Menu\ItemInterface
.
Items are created by a factory implementing Knp\Menu\FactoryInterface
.
It's best to think of each ItemInterface
object as an <li>
tag that can
hold children objects (<li>
tags that are wrapped in a <ul>
tag).
For example:
<?php
use Knp\Menu\Matcher\Matcher;
use Knp\Menu\MenuFactory;
use Knp\Menu\Renderer\ListRenderer;
$factory = new MenuFactory();
$menu = $factory->createItem('My menu');
$menu->addChild('Home', ['uri' => '/']);
$menu->addChild('Comments', ['uri' => '#comments']);
$menu->addChild('Symfony', ['uri' => 'http://symfony.com/']);
$renderer = new ListRenderer(new Matcher());
echo $renderer->render($menu);
The above would render the following html code:
<ul>
<li class="first">
<a href="/">Home</a>
</li>
<li class="current">
<a href="#comments">Comments</a>
</li>
<li class="last">
<a href="http://symfony.com/">Symfony</a>
</li>
</ul>
NOTE The menu framework automatically adds
first
andlast
classes to each<li>
tag at each level for easy styling. Notice also that acurrent
class is added to the "current" menu item by uri andcurrent_ancestor
to its ancestors (the classes are configurable) The above example assumes the menu is being rendered on the/comments
page, making the Comments menu the "current" item.
When the menu is rendered, it's actually spaced correctly so that it appears
as shown in the source html. This is to allow for easier debugging and can
be turned off by passing the true
as the second argument to the renderer.
<?php
// ...
$renderer = new ListRenderer(new Matcher(), ['compressed' => true]);
echo $renderer->render($menu);
You can also compress (or not compress) on a menu-by-menu basis by using the
compressed
option:
<?php
// ...
$renderer = new ListRenderer(new Matcher());
echo $renderer->render($menu, ['compressed' => true]);
Note: You can customize the rendering by extending the ListRenderer
and
overwrite some of its methods. If you use the TwigRenderer, you can overwrite
templates. Or you can provide your own implementation of the RendererInterface
.
Your menu tree works and acts like a multidimensional array. Specifically, it implements ArrayAccess, Countable and Iterator:
<?php
use Knp\Menu\MenuFactory;
$factory = new MenuFactory();
$menu = $factory->createItem('My menu');
$menu->addChild('Home', ['uri' => '/']);
$menu->addChild('Comments');
// ArrayAccess
$menu['Comments']->setUri('#comments');
$menu['Comments']->addChild('My comments', ['uri' => '/my_comments']);
// Countable
echo count($menu); // returns 2
// Iterator
foreach ($menu as $child) {
echo $child->getLabel();
}
As you can see, the name you give your menu item (e.g. overview, comments) when creating it is the name you'll use when accessing it. By default, the name is also used when displaying the menu, but that can be overridden by setting the menu item's label (see below).
There are many ways to customize the output of each menu item. Each property can be customized in two ways: either by passing it as an option when creating the item, or by using the setter of an existing item.
By default, a menu item uses its name when rendering. You can easily change this without changing the name of your menu item by setting its label:
<?php
// Setting the label when creating the item
$menu->addChild('Home', ['uri' => '/', 'label' => 'Back to homepage']);
// Changing the label of an existing item
$menu['Home']->setLabel('Back to homepage');
If an item isn't given a url, then text will be output instead of a link:
<?php
$menu->addChild('Not a link');
$menu->addChild('Home', '/');
$menu->addChild('Symfony', 'http://www.symfony-reloaded.org');
You can also specify the uri after creation via the setUri()
method:
<?php
$menu['Home']->setUri('/');
NOTE If you want to remove the uri of an item, set it to
null
.
In fact, you can add any attribute to the <li>
tag of a menu item. This
can be done when creating a menu item or via the setAttribute()
and setAttributes()
methods:
<?php
$menu->addChild('Home', ['attributes' => ['id' => 'back_to_homepage']]);
$menu['Home']->setAttribute('id', 'back_to_homepage');
NOTE
setAttributes()
will overwrite all existing attributes.
NOTE To remove an existing attribute, set it to
null
. It will not be rendered.
You can also add link attributes (displayed on the <a>
element), label
attributes (displayed on the <span>
element when it is not a link) or
children attributes (rendered on the <ul>
containing the list of children):
<?php
$menu->addChild('KnpLabs.com', ['uri' => 'http://knplabs.com']);
$menu['KnpLabs.com']->setLinkAttribute('class', 'external-link');
$menu->addChild('Not a link');
$menu['Not a link']->setLabelAttribute('class', 'no-link-span');
$menu->setChildrenAttribute('class', 'pull-left');
NOTE For the root element, only the children attributes are used as only the
<ul>
element is displayed.
NOTE In the 1.0 version of the library, the attributes were rendered on the root element instead of rendering the children attributes, which was inconsistent and has been changed for 1.1.
If you need to render only part of your menu, the menu framework gives you unlimited control to do so:
<?php
// render only 2 levels deep (root, parents, children)
$renderer->render($menu, ['depth' => 2]);
// rendering everything except for the children of the Home branch
$menu['Home']->setDisplayChildren(false);
$renderer->render($menu);
// render everything except for Home AND its children
$menu['Home']->setDisplay(false);
$renderer->render($menu);
Using the above controls, you can specify exactly which part of your menu you need to render at any given time.
Most renderers also support several other options, which can be passed as
the second argument to the render()
method:
depth
matchingDepth
: The depth of the scan to determine whether an item is an ancestor of the current item.currentAsLink
(default:true
): Whether to render the "current" menu item as link or as span.currentClass
(default:current
)ancestorClass
(default:current_ancestor
)firstClass
(default:first
)lastClass
(default:last
)compressed
(default:false
)allow_safe_labels
(default:false
)clear_matcher
(defaulttrue
): whether to clear the internal cache of the matcher after renderingleaf_class
(default:null
): class for leaf elements in your html treebranch_class
(default:null
): class for branch elements in your html tree
NOTE When setting the
allow_safe_labels
option totrue
, you can specify that a label should not be escaped by the renderer by adding thesafe_label
extra in the item. Use it with caution as it can create some XSS holes in your application if the label is coming from the user.
If the menu item is matched as current, a current
class will be added to
the li
around that item, as well as a current_ancestor
around any of
its parent li
elements. This state can either be forced on the item by
setting it explicitly or matched using several voters.
By default, the current item is rendered as a link too. You can make the current
item not a link by setting the currentAsLink
option to false. The ListRenderer
then renders the item with a <span>
tag instead of an <a>
.
<?php
use Knp\Menu\Matcher\Matcher;
use Knp\Menu\Matcher\Voter\UriVoter;
use Knp\Menu\MenuFactory;
use Knp\Menu\Renderer\ListRenderer;
$factory = new MenuFactory();
$menu = $factory->createItem('My menu');
// set the current state explicitly
$menu['current_item']->setCurrent(true);
$menu['non_current_item']->setCurrent(false);
// Use the voter
$menu['other_item']->setCurrent(null); // default value for items
$matcher = new Matcher();
$matcher->addVoter(new UriVoter($_SERVER['REQUEST_URI']));
$renderer = new ListRenderer($matcher);
The library provides 3 implementations of the VoterInterface:
Knp\Menu\Matcher\Voter\UriVoter
matching against the uri of the itemKnp\Menu\Matcher\Voter\RouteVoter
matching the_route
attribute of a Symfony Request object against theroutes
extra of the itemKnp\Menu\Matcher\Voter\RegexVoter
matching against the uri of the item using a regular expression
Here are some examples for instantiation of voters:
<?php
$regexVoter = new \Knp\Menu\Matcher\Voter\RegexVoter('/^StartOfUri/');
$routeVoter = new \Knp\Menu\Silex\Voter\RouteVoter();
$routeVoter->setRequest($symfonyRequest);
See Advanced Menu documentation page
$renderer = new ListRenderer(new Matcher(), [], 'ISO-8859-1');