id4v/menu-bundle
ID4V MenuBundle is a PHP package for defining, organizing, and rendering configurable navigation menus. Build menu structures with items and nested links, then output them consistently across your application. Its simple API helps keep menu configuration maintainable and reusable.
Read this first: it is not a Laravel package. id4v/menu-bundle is a Symfony bundle built for the Symfony 2.x/3.x era and depends on sonata-project/admin-bundle and Doctrine ORM. You cannot composer require it into a Laravel app — Composer will fail on symfony/symfony, symfony/framework-bundle, and Sonata constraints long before autoload. Treat this assessment as "what can I learn from / port out of it," not "what can I install."
Minimal steps if you find yourself in a legacy Symfony app that already has it:
composer require id4v/menu-bundle (expect to pin symfony/* to ~2.8|~3.0 and Sonata Admin to 2.x/3.x).app/AppKernel.php (new Id4v\MenuBundle\Id4vMenuBundle()), and import its admin routing in app/config/routing.yml so the Sonata screens appear.app/console doctrine:schema:update --dump-sql and inspect before executing — the bundle ships entity mappings that will create menu/menu-item tables.src/ first. Look at the Entity/ directory (the menu and menu-item models and their parent/position relationship), then Admin/ (the Sonata admin classes), then Resources/views/ (the render templates). Those three directories are the package; there is very little else.For a Laravel codebase, the fastest path to value is to copy the data model, not the code: a menus table (id, name, slug) and a menu_items table (id, menu_id, parent_id, label, route/url, target, position, enabled). That is ~40 lines of migration and gets you 90% of what this bundle does.
Model shape. The bundle's core idea is a Menu aggregate containing self-referencing MenuItem rows ordered by an integer position, with parent_id expressing nesting. It is adjacency-list, not nested-set — cheap to write, recursive to read. Mirror that exactly if you port it; don't "improve" it to nested set unless you actually need deep trees.
Route-name vs raw URL. The useful pattern here is storing either a Symfony route name plus serialized parameters or a literal URL, and resolving the route at render time. Copy this into Laravel as route_name + route_params (JSON) columns with a url fallback, and resolve with route($name, $params) in the accessor. It keeps menus portable across environments where absolute URLs differ.
Resolve-then-render, once per request. Fetch the menu by slug (header, footer), hydrate the full item set in a single query, build the tree in PHP, and hand a nested array to the view. Do not lazy-load children per node — that's the classic N+1 this kind of bundle invites.
// Laravel port of the pattern
$items = MenuItem::where('menu_id', $menu->id)
->where('enabled', true)
->orderBy('position')
->get();
$tree = $items->groupBy('parent_id'); // then recurse on $tree[null], $tree[$id]
Recursive partial. One Blade partial that includes itself:
<ul>
@foreach ($nodes as $node)
<li>
<a href="{{ $node->resolvedUrl() }}" target="{{ $node->target }}">{{ $node->label }}</a>
@if (!empty($node->children))
@include('partials.menu-items', ['nodes' => $node->children])
@endif
</li>
@endforeach
</ul>
Admin integration. In Sonata the pattern is: one Admin class per entity, a form field of type sonata_type_model_list for the parent, and position as a numeric input. The transferable lesson for Laravel is the same: expose menus through a resource controller (Filament/Nova/Backpack) with a parent select scoped to the same menu — never allow a parent from a different menu, or you get orphaned cross-links.
Cache the rendered HTML, invalidate on save. Menus change rarely and render on every page. Cache the built tree or the rendered string keyed by menu slug, and bust it in the model's saved/deleted events. This bundle leaves that to you; do it explicitly rather than relying on query caching.
Per-location menus. One menu per slot (header, footer, sidebar) looked up by slug is the cleanest contract and the one this bundle nudges you toward. Avoid a "one giant menu with visibility flags" design.
Admin base class signatures changed). Do not put this in a new build.config/bundles.php auto-registration.position, id to the ORDER BY when you port it.cascadeOnDelete on the parent foreign key and validate "parent is in the same menu and is not a descendant" in the form request.route_params as JSON, integers come back as ints but booleans/objects won't survive. Keep params to scalars.bin/console sonata:admin:list and bin/console debug:container | grep id4v; routing problems are almost always a missing routing.yml import or an ACL role lacking EDIT/LIST on the admin service.app/Resources/Id4vMenuBundle/views/ to shadow the bundle's template. In a port, that means keeping the recursive partial swappable per project rather than hard-coding markup in a component.spatie/laravel-menu or lavary/laravel-menu give you the builder/rendering side; for admin management, a plain menus + menu_items table plus your existing admin panel is less work than wiring an unfamiliar Symfony bundle. For Symfony today: knplabs/knp-menu-bundle (rendering) plus Sonata's own sonata-page-bundle, or just a custom entity.How can I help you explore Laravel packages today?