nucleos/menu-bundle
A Symfony bundle for building and rendering menus from a fluent PHP API, with support for nested items, attributes, translation, current-item matching, and Twig rendering. Configure menus in services and output accessible markup.
Install and register
composer require nucleos/menu-bundle
// config/bundles.php
Nucleos\MenuBundle\NucleosMenuBundle::class => ['all' => true],
As of 2.9.0 the bundle supports Symfony 8 in addition to earlier supported versions — the release adds framework compatibility rather than new API surface, so nothing in your day-to-day usage changes. Still verify the exact symfony/* and PHP range in vendor/nucleos/menu-bundle/composer.json against your app before committing; if you're on Symfony 8, upgrade to 2.9.0 to avoid constraint conflicts.
Where to look first. Before writing anything, open vendor/nucleos/menu-bundle/src and read, in order: the Attribute/ directory (AsMenu, MenuItem), the menu registry/locator, and DependencyInjection/. This bundle is small and low-adoption (6 stars, 0 dependents), so the source is the authoritative documentation — the README covers the happy path but not the edge cases. Also check composer.json for the Symfony/PHP constraint range before committing to it.
First use case: a static main navigation. Define one class per menu, discoverable by attribute:
namespace App\Menu;
use Nucleos\MenuBundle\Attribute\AsMenu;
use Nucleos\MenuBundle\Attribute\MenuItem;
#[AsMenu('main')]
final class MainMenu
{
#[MenuItem]
public function home(): array
{
return ['label' => 'menu.home', 'route' => 'app_home'];
}
#[MenuItem]
public function blog(): array
{
return ['label' => 'menu.blog', 'route' => 'app_blog_index'];
}
}
Then render it in your base layout. This bundle builds KnpMenu ItemInterface trees rather than rendering them, so pair it with knp-menu-bundle (or the bundle's own Twig extension if one is shipped) for output:
{{ knp_menu_render('main', {'currentClass': 'active'}) }}
That is the whole loop: attribute → service-autoconfigured class → named menu → renderer. If it works once, everything else is variation.
One class per logical menu, one method per item. Small methods keep diffs readable and let you inject only what a given item needs. Return an array (or an ItemInterface if you need full control over setUri, setExtra, setAttribute). Use explicit ordering (priority on the attribute, or a single method returning an ordered array) rather than relying on PHP method-declaration order, which is not a contract you want to depend on.
Nesting via children. The common shape is a parent item whose definition carries child items — either inline arrays or by referencing child method names. Build the tree in PHP, not in Twig; conditional rendering of whole sections belongs in the menu definition so that caches, breadcrumbs, and sitemaps all see the same structure.
Route-driven vs. URI items. Prefer route + routeParameters so URLs stay correct when routes change and so the bundle can compute the active/current item from the request. Drop to uri only for external links or absolute URLs you generate yourself. Keep the route name in a constant or the definition file — a typo surfaces as a RouteNotFoundException at menu build time, which is at least loud.
Authorization belongs in the definition, not the template. Inject Security and return early from the method (or use the attribute's condition support) when the user lacks the role. Hiding an admin link at render time still leaves it in any JSON/breadcrumb export you build from the same tree.
Reuse the built tree outside Twig. Inject the menu provider/registry into a controller or service and walk the ItemInterface tree. This is the highest-leverage pattern: one definition powers the header nav, the footer, a breadcrumb trail, a sitemap.xml, and a mobile drawer. Decorate or wrap the registry if you need to add global items (a "logout" entry, an environment banner) without touching every menu class.
Translation keys, not literals. Labels like menu.home translated by the renderer's translator extension mean you can add locales without editing menus. If you return translated strings directly, your menu build becomes locale-dependent and defeats caching.
Testing. Menus are ordinary services — pull them from the container in a KernelTestCase, assert labels, routes, and children. A quick unit test catches the ordering and authorization regressions that are otherwise only visible by clicking around.
Attribute classes must actually be services. #[AsMenu] only works on classes that the container knows about. The classic failure is App\Menu\ being excluded from services.yaml, or the file living outside the autoconfigured src/ tree — the menu silently vanishes and knp_menu_render('main') renders nothing or throws "menu not found". Verify with bin/console debug:container --tag=<the bundle's tag>.
Definitions are built, then reused — keep them cheap. Don't hit the database or an HTTP client inside an item method. If an item depends on request data, inject RequestStack and accept that the menu must be rebuilt per request (which means it can't be cached). Anything expensive should be computed once in the constructor or a lazy service.
Watch for circular dependencies. Menus commonly want the router, security, and your own domain services; if one of those transitively needs the menu (e.g. a breadcrumb listener), you get a container cycle. Inject lazily or move the dependency behind a service call.
Prod cache. New attributes and routes require a cache clear in prod; in dev a stale container can also mask the change. When "the menu is wrong only on staging", clear the cache before debugging anything else.
Active-state matching needs the request. Current-item detection compares the current route and parameters against each item. Items pointing at routes with optional parameters or at paths that are prefixes of others will match unexpectedly — set the "exact match" option or handle it in the renderer's currentClass logic.
Ordering is not guaranteed by method order. Use priority/weight from the attribute. If the bundle version you have lacks it, collapse the menu into one method returning an ordered array; it is uglier but deterministic.
Extend via decoration, not editing. Because definitions are plain services, a decorator around the registry/locator is cleaner than a base class, and it survives upgrades (the bundle is young and its API surface is not frozen — pin a version in composer.json and read the changelog on update).
KnpMenu interop is your escape hatch. Items are KnpMenu items, so setExtra(), setAttribute(), setLinkAttribute() and custom renderer themes all work. When the bundle's attribute surface feels limiting, drop to the underlying item rather than fighting the abstraction.
Debugging tip. Dump the tree in a controller (dump($menu->getChildren())) — inspecting the actual built structure answers more questions than reading the attribute docs, especially for nesting and extras. If you have the KnpMenuBundle profiler, it shows the same tree per request.
How can I help you explore Laravel packages today?