toxic-lemurs/menu-builder
A Laravel package for building and managing dynamic, nested navigation menus. Define menu structures, control items, links, and ordering, then render them in Blade views. A lightweight helper for site navigation, sidebars, and admin panels.
Install and confirm it loads on your Laravel version first — not last.
composer require toxic-lemurs/menu-builder
The package targets Laravel 5 and last shipped a release in November 2020. Before writing any menu code:
composer.json in the package for its illuminate/* constraints. If you're on Laravel 8+, expect to either relax constraints in a fork or vendor the source into app/Support/MenuBuilder.config/app.php.php artisan vendor:publish --provider="ToxicLemurs\MenuBuilder\...ServiceProvider") and read config/ plus src/ — with 6 stars and zero dependents there is effectively no community documentation to fall back on, so the source is the docs.Where to look first
src/ — the builder class, item class, and renderer. Understand how items store attributes (label, URL/route, children, active flag) before building abstractions on top.First use case
Define one menu in a dedicated service provider and render it in your layout. Keep it trivial to prove the wiring works:
// app/Providers/MenuServiceProvider.php
public function boot()
{
MenuBuilder::make('main', function ($menu) {
$menu->add('Home', ['route' => 'home']);
$menu->add('Dashboard', ['route' => 'dashboard']);
});
}
{{-- resources/views/layouts/app.blade.php --}}
{!! MenuBuilder::get('main')->render() !!}
(Method names above follow the common Laravel menu-builder convention — verify against src/ before committing to them.) Once that renders, you have a working baseline to layer the patterns below onto.
Centralize menu definitions, never inline them in views. Put each menu in its own class or a MenuServiceProvider, tagged by name (main, sidebar, footer, admin). Views should only ask for a named menu, so restructuring navigation is a one-file change.
MenuBuilder::make('sidebar', function ($menu) {
$menu->add('Reports', ['route' => 'reports.index'])
->add('Settings', ['route' => 'settings.edit']);
});
Prefer route names over hardcoded URLs. Route-based items survive domain and prefix changes and give the builder a chance to compute the active state for you. Fall back to raw URLs only for external links.
Handle active state explicitly. Most builders of this vintage either infer active state from the current route or let you set it manually. If inference is unreliable (query strings, resource sub-routes, nested prefixes), compute it yourself and pass it in:
$isActive = request()->routeIs('reports.*');
$menu->add('Reports', ['route' => 'reports.index', 'active' => $isActive]);
Wrap this in a helper or a small MenuState class rather than repeating routeIs calls across providers.
Gate items by authorization at build time, not render time. Filter inside the menu callback so unauthorized users never receive the markup:
if (Gate::allows('view-reports')) {
$menu->add('Reports', ['route' => 'reports.index']);
}
Because menus are typically built per-request, this stays correct without cache invalidation — which is exactly why you should not cache menu HTML blindly (see Gotchas).
Use view composers or a Blade component as the integration seam. Rather than calling the facade throughout templates, expose a single menu('main') helper or <x-menu name="main" /> component. This isolates the package so that swapping to spatie/menu or lavary/laravel-menu later is a one-class change.
Compose nested menus recursively. For dropdowns and multi-level sidebars, keep children in the builder's native structure and render with a recursive partial. Avoid manual if (hasChildren) blocks scattered across templates.
Keep ordering deterministic. If items are added across multiple providers (e.g. a module registers its own nav entry), assign explicit order / priority attributes and sort once before rendering. Insertion order across service providers is easy to break accidentally.
Test menu structure, not markup. Assert on the item tree (labels, routes, visibility for a given user) in feature tests. HTML assertions are brittle and will break on styling changes unrelated to logic.
The maintenance risk is the headline issue. Six stars, zero dependents, and no release since November 2020 means: no Laravel 9/10 compatibility work, no security triage, and no upstream fixes. Treat it as vendored code you now own. Either fork it into packages/ with your own composer.json, or copy the classes into app/Support/ and drop the dependency.
Version constraints will bite on modern Laravel. The package's illuminate/support and illuminate/view constraints likely cap below your installed framework. Expect composer require to fail or silently downgrade other packages. If you must keep it as a dependency, use a path repository pointing at a patched fork rather than editing vendor/.
Facade aliasing and auto-discovery are the usual first-run failures. If you see Class 'MenuBuilder' not found, it's almost always a missing alias in config/app.php or a cached config (php artisan config:clear). On Laravel 5.4 and earlier, manual provider registration is mandatory.
Config caching hides dynamic menu state. If menus are built at boot with request-dependent logic (auth, current user), config:cache or opcache interactions can freeze the first-built version. Build menus lazily — inside the composer/component — not in a provider's register().
Escaping in {!! !!} is on you. Rendering raw HTML means labels sourced from user input or translations must be escaped manually. Wrap label output in e() inside any custom renderer.
Active-class collisions with your CSS framework. Builders often emit a fixed active class. Tailwind/Bootstrap variants may need is-active, bg-blue-100, etc. Look for a configurable class or a custom renderer hook before hacking it in Blade. This is the most common extension point you'll actually need.
Permission checks per item can turn into N+1 queries. If each item calls Gate::allows and your policy hits the database, a 20-item menu becomes 20 queries. Eager-load the relevant relations once at the top of the menu callback and check against the in-memory model.
Caching menus correctly is user-scoped. If you do cache the built tree, key it on user_id (or a permission fingerprint) plus locale. A shared cache key will leak admin navigation to guests.
Extend via a custom renderer or macros, not by editing vendor code. Most packages in this family expose a render() method or a blade directive you can override by binding your own implementation in the container. That keeps upgrades (or your eventual fork sync) survivable.
Have an exit plan. Given the dependency profile, decide up front whether this is a short-term scaffold or a permanent fixture. If the project has legs, budget a migration to a maintained alternative (spatie/laravel-menu is the common successor) and keep the menu('main') seam described above so the swap is mechanical rather than archaeological.
How can I help you explore Laravel packages today?