Skip to content

Instantly share code, notes, and snippets.

@nateevans
Created April 3, 2014 16:56
Show Gist options
  • Save nateevans/9958390 to your computer and use it in GitHub Desktop.
Save nateevans/9958390 to your computer and use it in GitHub Desktop.
KNP Menu Bundle with Bootstrap 3 and Font Awesome 4 (see http://bit.ly/1sd1rJr , thanks to Niels Mouthaan!)
{% extends '::base.html.twig' %}
{% block title %}My Awesome Page!{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css">
{% endblock %}
{% block body %}
<div class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid">
{# Brand and toggle get grouped for better mobile display #}
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#admin-navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Awesome!</a>
</div>
{# Collect the nav links, forms, and other content for toggling #}
<div class="collapse navbar-collapse" id="admin-navbar-collapse-1">
{{ knp_menu_render('AcmeHelloBundle:MenuBuilder:mainMenu', {'currentClass': 'active', 'template': 'AcmeHelloBundle:Menu:knp_menu.html.twig'}) }}
{{ knp_menu_render('AcmeHelloBundle:MenuBuilder:userMenu', {'currentClass': 'active', 'template': 'AcmeHelloBundle:Menu:knp_menu.html.twig'}) }}
</div>
</div>
</div>
<div class="content" style="margin-top: 60px;">
Content goes here!
</div>
{% endblock %}
{% block javascripts %}
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"></script>
<script type="text/javascript" src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
{% endblock %}
{% extends 'knp_menu.html.twig' %}
{% block item %}
{% import "knp_menu.html.twig" as macros %}
{% if item.displayed %}
{%- set attributes = item.attributes %}
{%- set is_dropdown = attributes.dropdown|default(false) %}
{%- set divider_prepend = attributes.divider_prepend|default(false) %}
{%- set divider_append = attributes.divider_append|default(false) %}
{# unset bootstrap specific attributes #}
{%- set attributes = attributes|merge({'dropdown': null, 'divider_prepend': null, 'divider_append': null }) %}
{%- if divider_prepend %}
{{ block('dividerElement') }}
{%- endif %}
{# building the class of the item #}
{%- set classes = item.attribute('class') is not empty ? [item.attribute('class')] : [] %}
{%- if matcher.isCurrent(item) %}
{%- set classes = classes|merge([options.currentClass]) %}
{%- elseif matcher.isAncestor(item, options.depth) %}
{%- set classes = classes|merge([options.ancestorClass]) %}
{%- endif %}
{%- if item.actsLikeFirst %}
{%- set classes = classes|merge([options.firstClass]) %}
{%- endif %}
{%- if item.actsLikeLast %}
{%- set classes = classes|merge([options.lastClass]) %}
{%- endif %}
{# building the class of the children #}
{%- set childrenClasses = item.childrenAttribute('class') is not empty ? [item.childrenAttribute('class')] : [] %}
{%- set childrenClasses = childrenClasses|merge(['menu_level_' ~ item.level]) %}
{# adding classes for dropdown #}
{%- if is_dropdown %}
{%- set classes = classes|merge(['dropdown']) %}
{%- set childrenClasses = childrenClasses|merge(['dropdown-menu']) %}
{%- endif %}
{# putting classes together #}
{%- if classes is not empty %}
{%- set attributes = attributes|merge({'class': classes|join(' ')}) %}
{%- endif %}
{%- set listAttributes = item.childrenAttributes|merge({'class': childrenClasses|join(' ') }) %}
{# displaying the item #}
<li{{ macros.attributes(attributes) }}>
{%- if is_dropdown %}
{{ block('dropdownElement') }}
{%- elseif item.uri is not empty and (not item.current or options.currentAsLink) %}
{{ block('linkElement') }}
{%- else %}
{{ block('spanElement') }}
{%- endif %}
{# render the list of children#}
{{ block('list') }}
</li>
{%- if divider_append %}
{{ block('dividerElement') }}
{%- endif %}
{% endif %}
{% endblock %}
{% block dividerElement %}
{% if item.level == 1 %}
<li class="divider-vertical"></li>
{% else %}
<li class="divider"></li>
{% endif %}
{% endblock %}
{% block linkElement %}
<a href="{{ item.uri }}"{{ macros.attributes(item.linkAttributes) }}>
{% if item.attribute('icon') is not empty %}
<i class="{{ item.attribute('icon') }}"></i>
{% endif %}
{{ block('label') }}
</a>
{% endblock %}
{% block spanElement %}
<span>{{ macros.attributes(item.labelAttributes) }}>
{% if item.attribute('icon') is not empty %}
<i class="{{ item.attribute('icon') }}"></i>
{% endif %}
{{ block('label') }}
</span>
{% endblock %}
{% block dropdownElement %}
{%- set classes = item.linkAttribute('class') is not empty ? [item.linkAttribute('class')] : [] %}
{%- set classes = classes|merge(['dropdown-toggle']) %}
{%- set attributes = item.linkAttributes %}
{%- set attributes = attributes|merge({'class': classes|join(' ')}) %}
{%- set attributes = attributes|merge({'data-toggle': 'dropdown'}) %}
<a href="#"{{ macros.attributes(attributes) }}>
{% if item.attribute('icon') is not empty %}
<i class="{{ item.attribute('icon') }}"></i>
{% endif %}
{{ block('label') }}
<b class="caret"></b>
</a>
{% endblock %}
{% block label %}{{ item.label|trans }}{% endblock %}
<?php
namespace Acme\HelloBundle\Menu;
use Knp\Menu\FactoryInterface;
use Symfony\Component\DependencyInjection\ContainerAware;
class MenuBuilder extends ContainerAware
{
public function mainMenu(FactoryInterface $factory, array $options)
{
$menu = $factory->createItem('root');
$menu->setChildrenAttribute('class', 'nav navbar-nav');
$menu->addChild('Projects', array('route' => 'acme_hello_projects'))
->setAttribute('icon', 'fa fa-list');
$menu->addChild('Employees', array('route' => 'acme_hello_employees'))
->setAttribute('icon', 'fa fa-group');
return $menu;
}
public function userMenu(FactoryInterface $factory, array $options)
{
$menu = $factory->createItem('root');
$menu->setChildrenAttribute('class', 'nav navbar-nav navbar-right');
/*
You probably want to show user specific information such as the username here. That's possible! Use any of the below methods to do this.
if($this->container->get('security.context')->isGranted(array('ROLE_ADMIN', 'ROLE_USER'))) {} // Check if the visitor has any authenticated roles
$username = $this->container->get('security.context')->getToken()->getUser()->getUsername(); // Get username of the current logged in user
*/
$menu->addChild('User', array('label' => 'Hi visitor'))
->setAttribute('dropdown', true)
->setAttribute('icon', 'fa fa-user');
$menu['User']->addChild('Edit profile', array('route' => 'acme_hello_profile'))
->setAttribute('icon', 'fa fa-edit');
return $menu;
}
}
<?php
namespace Acme\HelloBundle\Menu;
use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Voter\VoterInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class RequestVoter implements VoterInterface
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function matchItem(ItemInterface $item)
{
if ($item->getUri() === $this->container->get('request')->getRequestUri()) {
// URL's completely match
return true;
} else if($item->getUri() !== $this->container->get('request')->getBaseUrl().'/' && (substr($this->container->get('request')->getRequestUri(), 0, strlen($item->getUri())) === $item->getUri())) {
// URL isn't just "/" and the first part of the URL match
return true;
}
return null;
}
}
services:
acme.hello.menu.voter.request:
class: Acme\HelloBundle\Menu\RequestVoter
arguments:
- @service_container
tags:
- { name: knp_menu.voter }
@Finistere
Copy link

Thanks @nateevans !

@DougHayward
Copy link

Genius I'll give this a go later. Spent an hour trying to mod the knp_menu.twig last night to accomplish this but didn't get too far, hah.

@karol-may
Copy link

Thanks @nateevans !

@thePanz
Copy link

thePanz commented Mar 5, 2015

IMHO the "label" (as described http://symfony.com/doc/master/bundles/KnpMenuBundle/i18n.html)
should be:

{%- block label %}
{{ item.label|trans(
    item.getExtra('translation_params', {}),
    item.getExtra('translation_domain', 'messages')
) }}
{%- endblock %}

@totaltrash
Copy link

Thanks for this (and Niels for the original post). Here is an updated RequestVoter passing the RequestStack instead of the whole container:

<?php

namespace Acme\HelloBundle\Menu;

use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Voter\VoterInterface;
use Symfony\Component\HttpFoundation\RequestStack;

class RequestVoter implements VoterInterface
{
    private $requestStack;

    public function __construct(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }

    public function matchItem(ItemInterface $item)
    {
        $request = $this->requestStack->getCurrentRequest();

        if ($item->getUri() === $request->getRequestUri()) {
            // URL's completely match
            return true;
        } else if ($item->getUri() !== $request->getBaseUrl() . '/' 
            && substr($request->getRequestUri(), 0, strlen($item->getUri())) === $item->getUri()) {
            // URL isn't just "/" and the first part of the URL match
            return true;
        }

        return null;
    }
}

And the updated service file:

services:
    acme.hello.menu.voter.request:
        class: Acme\HelloBundle\Menu\RequestVoter
        arguments: [ @request_stack ]
        tags:
            - { name: knp_menu.voter }

Copy link

ghost commented Aug 26, 2015

Why should I add the RequestVoter? Everything works fine without it?
I don't understand the reason to add the RequestVoter.

@vladab
Copy link

vladab commented Sep 9, 2015

Thanks @nateevans

Just a little correction, the span element in knp_menu.html.twig is closed before the macros argument

<span>{{ macros.attributes(item.labelAttributes) }}>

it should be:

<span {{ macros.attributes(item.labelAttributes) }}>

Great work!

@red-smeg
Copy link

how do you use the divider element ?

@BondashMaster
Copy link

Hi. New here.
I'm trying to use this example.
But I get an error that says "Unable to find template "AppBundle:Menu:knp_menu.html.twig""
I create the knp_menu.html.twig inside the views folder in the App/Resources folder.

This is the part I dont understand:
{{ knp_menu_render('AppBundle:MenuBuilder:mainMenu', {'currentClass': 'active', 'template': 'AppBundle:Menu:knp_menu.html.twig'}) }}

Does the second parameter with the twig has to be under a menu folder inside the AppBundle???
Thanks in advance

@woutsanders
Copy link

Hi there,

Thank you for these snippets. I have created a derivative of it to let it work with KeenThemes Metronic Admin Template 4 (sidebar): https://gist.github.com/woutsanders/79b3551f2c4f07ddaab3c74fa9aba132
I'm sure some stuff could be done a bit more efficient, but it's my first experience with this bundle and thus things could be a bit messy here and there.

I couldn't have done it without this gist (as mentioned in my gist).

Again, thank you for the work Niels! And Nate for posting it here :-)

@Invis1ble
Copy link

I fixed several bugs in the template (and made some corresponding modification in builder):
https://gist.github.com/Invis1ble/503db8bb9ff6dafc52c8b2bf7c6fec42

@seyfer
Copy link

seyfer commented Apr 19, 2017

@JHGitty, the same question.

Why we need request voter ?

@snoop168
Copy link

just curious, but the extends line in knp_menu.html.twig refers back to itself so this ends up in an endless loop. Am I missing something? I put the twig file in my Resources/views folder however I feel like that's probably not where I am supposed to put it...

@garak
Copy link

garak commented Sep 13, 2017

Anyone got a solution for Bootstrap 4?

@lsv
Copy link

lsv commented Jan 2, 2018

Bootstrap 4

Just be aware that dropdowns my bootstrap 4, is not officially supported

As multiple level is not currently supported by bootstrap 4
This requires you to install
https://github.com/bootstrapthemesco/bootstrap-4-multi-dropdown-navbar
And set the the use_multilevel = true

{% extends 'knp_menu.html.twig' %}

{% macro setCssClassAttribute(item, type, add) %}
    {% set getter = 'get' ~ type %}
    {% set setter = 'set' ~ type %}
    {% set value = attribute(item, getter, ['class']) %}
    {% if value is iterable %}
        {% set value = value|join(' ') %}
    {% endif %}
    {% do attribute(item, setter, ['class', value ~ ' ' ~ add]) %}
{% endmacro %}

{% block item %}
    {% import "knp_menu.html.twig" as macros %}
    {#
    As multiple level is not currently supported by bootstrap 4
    This requires you to install
    https://github.com/bootstrapthemesco/bootstrap-4-multi-dropdown-navbar
    And set the the use_multilevel = true
    #}
    {% set use_multilevel = false %}

    {% if item.displayed %}
        {%- set attributes = item.attributes %}
        {%- set is_dropdown = attributes.dropdown|default(false) %}
        {%- set divider_prepend = attributes.divider_prepend|default(false) %}
        {%- set divider_append = attributes.divider_append|default(false) %}

        {# unset bootstrap specific attributes #}
        {%- set attributes = attributes|merge({'dropdown': null, 'divider_prepend': null, 'divider_append': null }) %}

        {%- if divider_prepend %}
            {{ block('dividerElement') }}
        {%- endif %}

        {# building the class of the item #}
        {%- set classes = item.attribute('class') is not empty ? [item.attribute('class'), 'nav-item'] : ['nav-item'] %}
        {%- if matcher.isCurrent(item) %}
            {%- set classes = classes|merge([options.currentClass]) %}
        {%- elseif matcher.isAncestor(item, options.depth) %}
            {%- set classes = classes|merge([options.ancestorClass]) %}
        {%- endif %}
        {%- if item.actsLikeFirst %}
            {%- set classes = classes|merge([options.firstClass]) %}
        {%- endif %}
        {%- if item.actsLikeLast %}
            {%- set classes = classes|merge([options.lastClass]) %}
        {%- endif %}

        {# building the class of the children #}
        {%- set childrenClasses = item.childrenAttribute('class') is not empty ? [item.childrenAttribute('class')] : [] %}
        {%- set childrenClasses = childrenClasses|merge(['menu_level_' ~ item.level]) %}

        {# adding classes for dropdown #}
        {%- if is_dropdown %}
            {%- set classes = classes|merge(['dropdown']) %}
            {%- set childrenClasses = childrenClasses|merge(['dropdown-menu']) %}
        {%- endif %}

        {# putting classes together #}
        {%- if classes is not empty %}
            {%- set attributes = attributes|merge({'class': classes|join(' ')}) %}
        {%- endif %}
        {%- set listAttributes = item.childrenAttributes|merge({'class': childrenClasses|join(' ') }) %}

        <li{{ macros.attributes(attributes) }}>
            {# displaying the item #}
            {%- if is_dropdown %}
                {{ block('dropdownElement') }}
            {%- elseif item.uri is not empty and (not item.current or options.currentAsLink) %}
                {{ block('linkElement') }}
            {%- else %}
                {{ block('spanElement') }}
            {%- endif %}
            {%- if divider_append %}
                {{ block('dividerElement') }}
            {%- endif %}
            {% if item.hasChildren and options.depth is not same as(0) and item.displayChildren %}
                {{ block('dropdownlinks') }}
            {% endif %}
        </li>
    {% endif %}
{% endblock %}

{% block dropdownlinks %}
    {% if use_multilevel %}
        <ul class="dropdown-menu">
    {% else %}
        <div class="dropdown-menu">
    {% endif %}
    {% for item in item.children %}
        {{ block('renderDropdownlink') }}

        {% if use_multilevel and item.hasChildren and options.depth is not same as(0) and item.displayChildren %}
            {{ block('dropdownlinks') }}
        {% endif %}
    {% endfor %}
    {% if not use_multilevel %}
        </div>
    {% else %}
        </ul>
    {% endif %}
{% endblock %}

{% block renderDropdownlink %}
    {% import _self as ownmacro %}
    {%- set divider_prepend = item.attributes.divider_prepend|default(false) %}
    {%- set divider_append = item.attributes.divider_append|default(false) %}
    {%- set attributes = item.attributes|merge({'dropdown': null, 'divider_prepend': null, 'divider_append': null }) %}

    {% if use_multilevel %}
        <li>
    {% endif %}

    {%- if divider_prepend %}
        {{ block('dividerElementDropdown') }}
    {%- endif %}

    {%- if item.uri is not empty and (not item.current or options.currentAsLink) %}
        {{ ownmacro.setCssClassAttribute(item, 'LinkAttribute', 'dropdown-item') }}
        {{ block('linkElement') }}
    {%- else %}
        {{ block('spanElementDropdown') }}
    {%- endif %}

    {%- if divider_append %}
        {{ block('dividerElementDropdown') }}
    {%- endif %}

    {% if use_multilevel %}
        </li>
    {% endif %}
{% endblock %}

{% block spanElementDropdown %}
    {% import "knp_menu.html.twig" as macros %}
    {% import _self as ownmacro %}
    {{ ownmacro.setCssClassAttribute(item, 'LabelAttribute', 'dropdown-header') }}
    <div {{ macros.attributes(item.labelAttributes) }}>
        {% if item.attribute('icon') is not empty  %}
            <i class="{{ item.attribute('icon') }}"></i>
        {% endif %}
        {{ block('label') }}
	</div>
{% endblock %}

{% block dividerElementDropdown %}
    <div class="dropdown-divider"></div>
{% endblock %}

{% block dividerElement %}
    {% if item.level == 1 %}
        <li class="divider-vertical"></li>
    {% else %}
        <li class="divider"></li>
    {% endif %}
{% endblock %}

{% block linkElement %}
    {% import "knp_menu.html.twig" as macros %}
    {% import _self as ownmacro %}
    {{ ownmacro.setCssClassAttribute(item, 'LinkAttribute', 'nav-link') }}
    <a href="{{ item.uri }}"{{ macros.attributes(item.linkAttributes) }}>
        {% if item.attribute('icon') is not empty  %}
            <i class="{{ item.attribute('icon') }}"></i>
        {% endif %}
        {{ block('label') }}
    </a>
{% endblock %}

{% block spanElement %}
    {% import "knp_menu.html.twig" as macros %}
    {% import _self as ownmacro %}
    {{ ownmacro.setCssClassAttribute(item, 'LabelAttribute', 'navbar-text') }}
    <span {{ macros.attributes(item.labelAttributes) }}>
        {% if item.attribute('icon') is not empty  %}
            <i class="{{ item.attribute('icon') }}"></i>
        {% endif %}
        {{ block('label') }}
	</span>
{% endblock %}

{% block dropdownElement %}
    {% import "knp_menu.html.twig" as macros %}
    {%- set classes = item.linkAttribute('class') is not empty ? [item.linkAttribute('class')] : [] %}
    {%- set classes = classes|merge(['dropdown-toggle', 'nav-link']) %}
    {%- set attributes = item.linkAttributes %}
    {%- set attributes = attributes|merge({'class': classes|join(' ')}) %}
    {%- set attributes = attributes|merge({'data-toggle': 'dropdown'}) %}
    <a href="#"{{ macros.attributes(attributes) }}>
        {% if item.attribute('icon') is not empty  %}
            <i class="{{ item.attribute('icon') }}"></i>
        {% endif %}
        {{ block('label') }}
        <b class="caret"></b>
    </a>
{% endblock %}

{% block label %}{{ item.label|trans }}{% endblock %}

Works with the following

        $menu = $event->getItem();
        $menu->addChild(
            'linking',
            [
                'route' => 'profile_index',
            ]
        );

        $menu->addChild(
            'texting',
            [
                'labelAttributes' => [
                    'class' => 'class3 class4',
                ],
            ]
        );

        $dropdown = $menu->addChild(
            'Hello Me',
            [
                'attributes' => [
                    'dropdown' => true,
                ],
            ]
        );

        $dropdown->addChild(
            'Profile',
            [
                'route' => 'profile_index',
                'attributes' => [
                    'divider_append' => true,
                ],
            ]
        );

        $dropdown->addChild(
            'text',
            [
                'attributes' => [
                    'icon' => 'fa fa-user-circle',
                ],
                'labelAttributes' => [
                    'class' => ['class1', 'class2'],
                ],
            ]
        );

        $dropdown->addChild(
            'Logout',
            [
                'route' => 'logout',
                'attributes' => [
                    'divider_prepend' => true,
                    'icon' => 'fa fa-sign-out',
                ],
            ]
        );

@chmielot
Copy link

chmielot commented Feb 6, 2018

@nateevans Why is the RequestVoter required (it works without it)?

@4d4ch4u32
Copy link

Got error: Accessing Twig_Template attributes is forbidden.

@Warxcell
Copy link

what is $event variable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment