Installation
composer require braunstetter/menu-bundle
Register the bundle in config/bundles.php:
return [
// ...
Braunstetter\MenuBundle\MenuBundle::class => ['all' => true],
];
First Menu Creation
Create a menu class (e.g., src/Menu/NavigationMenu.php):
namespace App\Menu;
use Braunstetter\MenuBundle\Menu\Menu;
use Braunstetter\MenuBundle\Menu\MenuItem;
class NavigationMenu extends Menu
{
public function build(): void
{
$this->addItem(new MenuItem('Home', 'home'))
->addChild(new MenuItem('About', 'about'))
->addChild(new MenuItem('Contact', 'contact'));
}
}
Register the Menu
Add it to config/packages/braunstetter_menu.yaml:
menus:
navigation: App\Menu\NavigationMenu
First Render Use Twig in your template:
{{ render(controller('braunstetter_menu.controller.menu:render', {'menuName': 'navigation'})) }}
Extend the menu with dynamic items (e.g., user-specific links):
public function build(): void
{
$this->addItem(new MenuItem('Home', 'home'));
if (auth()->check()) {
$this->addItem(new MenuItem('Dashboard', 'dashboard'));
}
}
Nested Menus
$parent = new MenuItem('Products', 'products');
$parent->addChild(new MenuItem('Laptops', 'laptops'));
$parent->addChild(new MenuItem('Phones', 'phones'));
$this->addItem($parent);
Conditional Items
$this->addItem(
new MenuItem('Admin', 'admin')
->setActiveWhen(function () {
return request()->is('admin/*');
})
);
Extend for Breadcrumbs
class BreadcrumbMenu extends Menu
{
public function build(): void
{
$this->addItem(new MenuItem('Home', 'home'));
if (request()->is('products/*')) {
$this->addItem(new MenuItem('Products', 'products'));
}
}
}
Share Across Controllers Inject the menu service:
use Braunstetter\MenuBundle\Menu\MenuManagerInterface;
public function __construct(private MenuManagerInterface $menuManager) {}
public function show()
{
$menu = $this->menuManager->getMenu('navigation');
// Use $menu->getItems() as needed
}
// src/EventListener/MenuListener.php
use Braunstetter\MenuBundle\Event\MenuEvent;
class MenuListener
{
public function onMenuBuild(MenuEvent $event)
{
if ($event->getMenuName() === 'navigation') {
$event->getMenu()->addItem(new MenuItem('Custom Item', 'custom'));
}
}
}
Register in services.yaml:
services:
App\EventListener\MenuListener:
tags:
- { name: kernel.event_listener, event: menu.build, method: onMenuBuild }
Render Blocks
{% render_menu 'navigation' %}
{% render_menu 'navigation' with {'template': 'AppBundle:Menu:custom.html.twig'} %}
Custom Templates
Override default templates in templates/braunstetter_menu/:
{# templates/braunstetter_menu/menu.html.twig #}
<ul>
{% for item in items %}
<li class="{{ item.active ? 'active' }}">{{ item.label }}</li>
{% endfor %}
</ul>
Caching Issues
php bin/console cache:clear
config/packages/braunstetter_menu.yaml:
menus:
navigation: App\Menu\NavigationMenu
cache: false
Active Item Logic
setActiveWhen() uses a callable (closure or method):
// ❌ Wrong (string)
$item->setActiveWhen('request()->is("home")');
// ✅ Correct (closure)
$item->setActiveWhen(fn() => request()->is("home"));
Circular Dependencies
Dump Menu Structure
$menu = $this->menuManager->getMenu('navigation');
dump($menu->getItems()); // Inspect raw data
Check Event Firing
php bin/console debug:event-dispatcher
Template Overrides
templates/braunstetter_menu/).Custom Matchers
Extend Braunstetter\MenuBundle\Matcher\MatcherInterface for unique active-item logic:
class CustomMatcher implements MatcherInterface
{
public function isActive(MenuItem $item): bool
{
return request()->user()?->isAdmin();
}
}
Register in services.yaml:
services:
App\Matcher\CustomMatcher:
tags:
- { name: braunstetter_menu.matcher }
Dynamic Menu Loading
Use MenuBuilderInterface to load menus dynamically:
$menu = $this->menuManager->getMenu('navigation', ['dynamic' => true]);
Localization Pass translations to menu items:
$this->addItem(new MenuItem($this->translator->trans('menu.home'), 'home'));
LazyBuildableMenuInterface.{% cache app.menu_navigation (app.user) %}
{{ render_menu('navigation') }}
{% endcache %}
How can I help you explore Laravel packages today?