jbouzekri/config-knp-menu-bundle
A Symfony bundle that brings configuration-driven menus to KnpMenuBundle, letting you define navigation menus in YAML instead of PHP. It simplifies menu setup and maintenance while leveraging KnpMenu’s rendering, extensions, and Symfony integration.
Prerequisites: the bundle is a configuration layer on top of KnpMenuBundle, not a replacement. Install and configure knp-menu-bundle first (Twig helper, knp_menu.menu_provider), otherwise there is nothing for this package to feed.
composer require jbouzekri/config-knp-menu-bundle
Register it in config/bundles.php (Flex may not do this automatically given the low install count):
Jbouzekri\ConfigKnpMenuBundle\ConfigKnpMenuBundle::class => ['all' => true],
First config file — in a Symfony Flex app, config/packages/config_knp_menu.yaml (pre-Flex: app/config/config.yml under the bundle's root key, commonly config_knp_menu:):
config_knp_menu:
menus:
main:
children:
home:
label: 'Home'
route: 'app_home'
catalog:
label: 'Catalog'
uri: '#'
children:
products:
label: 'Products'
route: 'app_product_index'
First use case: delete the hardcoded <ul> from your base.html.twig and replace it with:
{{ knp_menu_render('main', {'currentClass': 'active'}) }}
Where to look first (in this order):
vendor/jbouzekri/config-knp-menu-bundle/README.md — the only real docs.DependencyInjection/Configuration.php — this is the authoritative schema. Every key (route, uri, label, attributes, childrenAttributes, extras, display, …) is defined there. When the README is stale, trust this file.bin/console config:dump-reference config_knp_menu — prints the same tree with defaults, without reading source.DependencyInjection/ConfigKnpMenuExtension.php — shows how bundles' YAML files are discovered and merged, which is the actual selling point of the package.Then run bin/console cache:clear and check bin/console debug:config config_knp_menu.
Per-bundle fragments (the reason to use this package). Each bundle ships its own menu YAML (Resources/config/config_knp_menu.yaml or whatever convention the extension loads — confirm in the extension) and the app-level file contributes the app-specific items. The DI extension merges everything into one menu tree. Prefix item names per bundle (shop_products, admin_users) so two bundles can never clash on a key.
Nested/dropdown menus work through children; the parent typically gets uri: '#' and CSS attributes:
catalog:
label: 'Catalog'
uri: '#'
attributes:
class: 'dropdown-toggle'
childrenAttributes:
class: 'dropdown-menu'
children:
products:
label: 'Products'
route: 'app_product_index'
routeParameters: { page: 1 }
Styling and icons are done via attributes / linkAttributes / extras. Use knp_menu_render('main', {'template': 'menu/main.html.twig'}) when the default markup isn't enough.
Structured metadata in extras (new in 7.1, PR #26). extras now accepts nested maps and lists instead of only flat scalars, so per-item presentation data that used to require a compiler pass or an event subscriber can live in YAML:
products:
label: 'Products'
route: 'app_product_index'
extras:
icon: { name: 'fa-box', style: 'solid' }
badge: { text: 'New', variant: 'success' }
analytics: { event: 'nav_click', params: ['section', 'catalog'] }
Read it in a custom Twig template with item.getExtra('icon').name / item.getExtra('badge').text (twig attribute() if you prefer explicit access). Anything you can read back through getExtra() — including nested arrays — is fair game, so this is the natural home for icons, badge copy, data-attribute maps, and analytics payloads. Keep leaf-level extras keys namespaced (icon, badge, analytics) rather than dumping a dozen top-level keys, and don't put behaviour here: routing, visibility, and access checks still belong in KnpMenuBundle's own options.
Mixed static + dynamic menus. The config-defined tree is fine for the stable 90%, but "My account" items depend on the logged-in user. Don't try to express that in YAML — subscribe to KnpMenuBundle's ConfigureMenuEvent (or write a small compiler pass) and mutate the menu after it's built:
public function onConfigureMenu(ConfigureMenuEvent $event): void
{
if ($event->getMenu()->getName() !== 'main' || !$this->security->getUser()) {
return;
}
$event->getMenu()->addChild('account', [
'route' => 'app_account',
'extras' => ['icon' => ['name' => 'fa-user', 'style' => 'solid']],
]);
}
Note the extras you pass to addChild() at runtime are plain KnpMenu extras — the 7.1 nesting support applies to the YAML path, so keep the array shape identical between the two sources if the same template renders both.
Environment tweaks. Because it's plain Symfony config, when@dev: blocks let you add a "Debug" menu entry only in dev without touching production output.
Testing. Inject Knp\Menu\Provider\MenuProviderInterface, call get('main'), and assert on getChildren() / getLabel() / getUri() / getExtra('icon')['name']. This exercises the YAML merge without booting a browser.
bin/console cache:clear, or cache:pool:clear cache.global_clearer in dev if opcache/FPM is involved). Half of "the bundle is broken" reports are stale config cache. This applies doubly to extras shape changes — an old compiled container keeps serving the old flat extras array, which is a confusing getExtra('icon')['name'] on null.children arrays across bundles is not obviously defined — some extensions use array_merge_recursive, some replace. Read ConfigKnpMenuExtension before relying on a bundle overriding an app-level home: item; a same-named child may be replaced or duplicated depending on implementation. The same question now applies inside extras: verify empirically whether a bundle fragment merging extras: {icon: {...}} into an existing item deep-merges the nested map or replaces the whole extras key.extras needs 7.1+. On earlier 7.x versions nested arrays under extras were not preserved, so an upgrade is required before moving structured metadata out of a subscriber. Pin the constraint (^7.1) rather than ^7.0 and check composer show jbouzekri/config-knp-menu-bundle on CI, or you'll get a flat/empty extras array depending on the machine.route: key throws RouteNotFoundException during rendering, i.e. a 500 on every page that renders the nav. Grep your config for routes when you delete/rename a controller action.display / roles, understand it as cosmetics only — hidden items are still routable. Enforce with access_control / voters on the actual routes.display: false still keeps the item in the tree (useful for isCurrent/breadcrumb maths) but a hidden parent hides its children. Prefer omitting the item when you don't need it at all.children.home in the same map — the last one wins with no warning. Prefix names.label values are passed through the translator. Set extras: {translation_domain: 'navigation'} (or the equivalent top-level key) rather than hardcoding English labels.DependencyInjection/ and menu-building classes into src/Menu/ and drop the bundle registration — you keep the YAML workflow and lose the maintenance question. The extras nesting is a couple of lines in the normaliser, so a vendored copy survives that feature gap easily.config:dump-reference (key exists?) → debug:config config_knp_menu (did my file get merged? is extras the shape I expect?) → debug:container --tag=knp_menu.provider → render the menu in a controller and dump($menu) / dump($menu->getChild('products')->getExtra('icon')).Overall: a genuinely nice idea (declarative menus merged across bundles) with a thin, barely-maintained codebase — though 7.1 proves someone other than the author can still get a patch in. Worth it for a small, stable nav tree in a monorepo of bundles; the nested-extras support removes one common reason to reach for an event subscriber. Budget an afternoon to read the extension and the Configuration class first, and keep a fallback plan to inline the ~few hundred lines if it drifts from KnpMenuBundle.
How can I help you explore Laravel packages today?