Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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

This comment has been minimized.

Copy link

Finistere commented Aug 18, 2014

Thanks @nateevans !

@BonnieDoug

This comment has been minimized.

Copy link

BonnieDoug commented Dec 15, 2014

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-gontarski

This comment has been minimized.

Copy link

karol-gontarski commented Jan 20, 2015

Thanks @nateevans !

@thePanz

This comment has been minimized.

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

This comment has been minimized.

Copy link

totaltrash commented May 20, 2015

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 }
@JHGitty

This comment has been minimized.

Copy link

JHGitty 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

This comment has been minimized.

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

This comment has been minimized.

Copy link

red-smeg commented Feb 28, 2016

how do you use the divider element ?

@BondashMaster

This comment has been minimized.

Copy link

BondashMaster commented May 18, 2016

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

@drMacFaulty

This comment has been minimized.

Copy link

drMacFaulty commented Jan 11, 2017

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

This comment has been minimized.

Copy link

Invis1ble commented Feb 5, 2017

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

@seyfer

This comment has been minimized.

Copy link

seyfer commented Apr 19, 2017

@JHGitty, the same question.

Why we need request voter ?

@snoop168

This comment has been minimized.

Copy link

snoop168 commented May 17, 2017

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

This comment has been minimized.

Copy link

garak commented Sep 13, 2017

Anyone got a solution for Bootstrap 4?

@lsv

This comment has been minimized.

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

This comment has been minimized.

Copy link

chmielot commented Feb 6, 2018

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

@antondachauer

This comment has been minimized.

Copy link

antondachauer commented Aug 5, 2018

Got error: Accessing Twig_Template attributes is forbidden.

@Warxcell

This comment has been minimized.

Copy link

Warxcell commented Aug 10, 2018

what is $event variable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.