zicht/menu-bundle
ZichtMenuBundle provides a flexible, configurable menu system for Symfony applications. Define hierarchical menus, attach routes or URLs to items, control visibility with security checks, and render navigation in Twig using customizable templates and item types.
Coming from Laravel, the mental model shifts: this is a Symfony bundle, not a Composer package you php artisan into. There is no service provider auto-discovery — Symfony's bundles.php + Flex recipe wiring does that job.
Minimal steps:
composer require zicht/menu-bundle (verify the constraints against your Symfony version — this is a small, low-traffic package, so pin compatible versions explicitly).Zicht\MenuBundle\ZichtMenuBundle in config/bundles.php. In Symfony 4+/Flex this may be automatic; check config/bundles.php before assuming.Menu and MenuItem). Run bin/console doctrine:schema:update --dump-sql first — do not --force until you've read the diff. Alternatively generate a migration: bin/console doctrine:migrations:diff.knplabs/knp-menu-bundle (the conventional dependency for menu bundles). If KnpMenuBundle isn't installed, install it — this package almost certainly builds on top of it rather than replacing it.src/Zicht/MenuBundle/ — specifically:
Entity/ for Menu / MenuItem (your data contract),Menu/ or Builder/ for the runtime builder,DependencyInjection/Configuration.php for configurable options.Menu row, add a MenuItem pointing to an existing route, then render it in a template with knp_menu_render('your_menu_alias') (or the bundle's equivalent alias).If you can't find a README example that matches your Symfony version, stop and read the Resources/config/services.xml — Symfony bundles from this era often wire services with XML, and that file is the real source of truth for service IDs and aliases you'll inject.
Menu as persisted data, not code. Unlike Laravel's spatie/laravel-menu (where you build menus in PHP closures), this bundle's value is that menus live in the database as Menu / MenuItem rows. Treat it as a content type, not config. Your admin CRUD (Sonata, EasyAdmin, or custom) is where editors compose the menu; your templates just render it.
Repository → builder → KnpMenu. The typical flow:
$menuFactory->getMenu('primary-nav') or similar).Register your own alias-to-menu mapping in services.yaml so templates stay dumb:
# config/services.yaml
App\Menu\PrimaryNavFactory:
arguments:
$menuRepository: '@zicht_menu.menu_repository'
tags: ['knp_menu.factory']
Multi-locale menus. If you have multiple menus per locale, either create one Menu per locale or hang locale onto MenuItem's URL resolution. The safe pattern is one menu row per (alias, locale) pair, keyed by alias — then your resolver picks by $request->getLocale().
Route vs. URL items. Persist MenuItem entries that either reference a Symfony route name plus parameters or a raw URL. Prefer route names so router:match still works and so links survive domain changes. Resolve at render time, not at save time.
Doctrine relations & cascade. Menu ↔ MenuItem is parent/child with a self-referencing relation on MenuItem for nesting. Configure cascade: ['persist', 'remove'] on the Menu→items side and orphanRemoval: true — otherwise deleting a menu leaves orphaned items. This is the single most common production bug with tree entities.
Integration with KnpMenu's MenuBuilder. Extend the bundle's factory to attach route/uri via $builder->addChild(...)->setUri(...). If the bundle exposes a MenuItemInterface, implement it on your own entity subclass so custom fields (e.g. target, rel, icon) survive into the KnpMenu item object.
Testing. Use KernelTestCase with a real kernel and a test DB. Assert against the built Menu object ($menu->getChild('home')) rather than rendered HTML — HTML assertions against this bundle break between KnpMenu minor versions.
composer.json's symfony/* constraints first. If it requires Symfony 2.x/3.x and you're on 6/7, expect to swap MenuBuilder internals, ContainerAwareCommand, and any ServiceController references.Resources/config/services.xml (or services.php) for the exact service IDs — they're not always what the docs suggest, and the zicht_menu.* prefix is not guaranteed.MenuItem (very likely), you need to either subclass the entity (mapping inheritance — tricky) or fork/vendor the bundle. Decide early; retrofitting is painful.router and KnpMenu caching will happily serve stale menus after an editor changes a MenuItem. Hook Menu / MenuItem lifecycle events (postPersist, postUpdate, postRemove) to clear the relevant cache pool, or the menu will "not update" for ten minutes and eat an afternoon.bin/console debug:container zicht_menu (or with --show-private) to confirm services are registered. If knp_menu_render throws "no menu found for alias", the alias resolution is failing — trace MenuRepository::findOneByAlias() before touching the template.templates/bundles/ZichtMenuBundle/, (b) implementing the bundle's MenuItem interface on a subclass, or (c) replacing the factory service via a compiler pass. There's rarely a clean "configure and done" path for custom item attributes.MenuItem::url persisted as a raw string is an admin-only field in practice. If exposing it to non-trusted editors, sanitize on save — this bundle almost certainly does not.vendor/ via a VCS repo ("repositories": [{"type": "vcs", ...}]), and pin a commit hash — don't rely on Packagist as-is for a long-lived app.How can I help you explore Laravel packages today?