dick/menumanager
A Laravel menu management package for building, ordering, and rendering dynamic navigation menus and nested menu items. Define links to routes or URLs, control hierarchy, and output ready-to-use menus in Blade views, keeping site navigation easy to maintain and customize.
Install and migrate.
composer require dick/menumanager
php artisan vendor:publish --provider="Dick\MenuManager\MenuManagerServiceProvider"
php artisan migrate
This package predates Laravel's package auto-discovery, so on newer Laravel you may need to register the provider manually in config/app.php (or bootstrap/providers.php on Laravel 11+):
Dick\MenuManager\MenuManagerServiceProvider::class,
Look here first.
config/menumanager.php or similar) — route prefix, middleware, model binding, template that renders the menu.id, name, url, type, target, parent_id, and usually lft/rgt/depth from kalnoy/nestedset).MenuItem) — this is your integration surface; everything else is admin UI.First use case: log in to Dick Admin, open the menu manager route (typically /admin/menu-item), add two or three top-level items and drag one under another. Then render them in your layout:
@php($menu = \Dick\MenuManager\Models\MenuItem::whereNull('parent_id')->orderBy('lft')->get())
@include('partials.menu', ['items' => $menu])
At that point you have a full loop — editors change the menu without a deploy, you render it in Blade.
Recursive rendering in Blade. The tree is small, so render it as a partial that self-includes. This is the pattern the package expects (and what its own default view does):
{{-- resources/views/partials/menu.blade.php --}}
@if($items->isNotEmpty())
<ul class="menu">
@foreach($items as $item)
<li class="{{ $item->is_active ? 'active' : '' }}">
<a href="{{ $item->url }}" target="{{ $item->target }}">
{{ $item->name }}
</a>
@include('partials.menu', ['items' => $item->children])
</li>
@endforeach
</ul>
@endif
Eager-load children once. Fetching root nodes with with('children') avoids N+1 queries when rendering. For deeper nesting pull childrenRecursive (nested set) or eager-load children.children.
Cache the menu, invalidate on write. Menus are write-rare/read-constant, so caching is nearly free:
$menu = Cache::rememberForever('menu.main', fn () => MenuItem::with('children')->whereNull('parent_id')->orderBy('lft')->get());
Hook model events to bust it:
MenuItem::saved(fn () => Cache::forget('menu.main'));
MenuItem::deleted(fn () => Cache::forget('menu.main'));
// also cover restored/moved events if the package uses nested set
Scope by menu name if you need more than one. If the schema has a menu/handle/collection column, filter by it (where('menu', 'footer')) and cache per-scope. If not, either add the column in a migration or maintain "one menu table per menu" via the config — the former is cleaner.
Active state. Don't compare raw strings — URLs drift with trailing slashes, locales, and query strings. Add an accessor:
public function getIsActiveAttribute(): bool
{
$path = trim(parse_url($this->url, PHP_URL_PATH), '/');
return $this->url === url()->current()
|| ($path && request()->is($path . '*'));
}
Link to routes, not URLs, when possible. Store the route name (or a type + route column if the schema supports it) and resolve at render time. Hardcoded URLs are the most common source of broken menus after a refactor.
Customize the output. Publish the package views and override the menu partial rather than editing vendor files. Keep your override small — the package's default markup is a starting point, not a contract.
Nested set columns are the ground truth. If the table has lft/rgt/depth, never write those by hand and never insert raw rows with DB::table(). Use $parent->children()->create(...) or MenuItem::create(['parent_id' => $parent->id, ...]) — otherwise the tree corrupts and drag-and-drop starts behaving erratically. Run MenuItem::fixTree() (nested set) if it already has.
Drag-and-drop needs the package JS. Nesting/reordering is a JavaScript feature. If the admin loads but you can't drag, check that the package's assets are published and included by the admin layout — this is the #1 "the package is broken" report, and it's almost always an asset issue.
Cache is invisible until it isn't. If editors save a menu item and the site doesn't change, the cache invalidation hook is missing or firing on the wrong model. php artisan cache:clear confirms the diagnosis in seconds.
Config caching. After editing config/menumanager.php, run php artisan config:clear; on production the config is baked into the deploy.
Authorization. The manager route is registered under whatever admin middleware the package ships with. Verify a non-admin can't reach /admin/menu-item; if Dick Admin's guard wiring has drifted, add an explicit policy or route middleware.
Old package, newer Laravel. dick/menumanager targets the pre-Backpack, Laravel 5.x era. On modern Laravel expect provider/facade/signature breaks — either pin an older dependency set, fork and maintain, or migrate to backpack/menucrud (the maintained successor by the same author) with a one-time migration of the menu_items table.
Migrations collide with existing schemas. If you already have a menu_items or menus table, publishing the migration will fail on php artisan migrate. Rename the package table in the published migration before running it, and update the model's $table accordingly.
Seeding trees. For deterministic seeds, create parents first and children via $parent->children()->create(...); batch inserts of nested sets with mixed parent/child order produce incorrect lft/rgt values.
Localization. If item names need translating, don't add columns ad hoc — either add a translations table keyed by menu_item_id or duplicate per-locale menus with a locale column and filter at fetch time. Retrofitting either later is painful.
How can I help you explore Laravel packages today?