vespakoen/menu
Laravel package for building and rendering database-backed menus. Create multiple menus, nest and order items, attach links, and output navigation in Blade with a simple API. Useful for headers, sidebars, and admin navigation.
This is a nested-list/hierarchy package built around an ItemList object whose nodes carry a depth. Version 3.0.4 is a patch release: it stops the package from throwing when you request an ItemList at a depth that doesn't exist in the tree. Nothing was removed or renamed, so upgrading from 3.0.x is a composer update away — but the behavioural change is worth understanding before you rely on it (see Gotchas).
Minimal setup:
composer require <vendor>/<package>:"^3.0" — stay on the 3.x line; the fix is in 3.0.4, so pin ^3.0.4 if you were working around the old exception.php artisan vendor:publish --tag=<package>-config, then php artisan config:clear.ItemList class before the README. It is the whole API surface, and the depth handling is the only part that has changed recently — ten minutes in the source beats an hour of guessing.First use case: build a nested menu/sitemap/taxonomy once in a small action class, render it with a single recursive Blade partial, and serialize the same object to JSON for an API endpoint. That exercises construction, traversal (including depth lookups), and output — the three places this package earns its keep.
// app/Actions/BuildMenu.php
public function handle(): ItemList
{
return ItemList::make(
MenuItem::query()->orderBy('position')->get()
);
}
Build the list in one place. Construct the ItemList in an action or service class, not in a controller and never in Blade. The tree is a value object — build it, cache it or pass it, then render it.
Render recursively with a Blade component. Depth is most useful for choosing a partial or an indent level rather than for business logic:
{{-- resources/views/components/menu-item.blade.php --}}
<li class="depth-{{ $item->depth }}">
{{ $item->title }}
@if ($item->children->isNotEmpty())
<ul>
@foreach ($item->children as $child)
<x-menu-item :item="$child" />
@endforeach
</ul>
@endif
</li>
Exploit the 3.0.4 fix for optional branches. If you render a teaser view that only reaches two levels deep, you can now ask for a deeper slice of a shallow tree and get an empty result instead of an exception. That makes "show as much as exists" layouts trivial:
$preview = $list->atDepth(3); // empty, not an exception, on a 2-level tree
Cache the built tree, not the query. The object is expensive to assemble and cheap to serialize. Cache the array form with a key derived from the underlying model's updated_at, and bust it from an observer:
Cache::remember("menu.{$site->id}", 3600, fn () => $builder->handle()->toArray());
Test with fixtures, not the database. A handful of array fixtures — flat, one level, deeply nested, empty — will cover every traversal path in the package faster than factories will.
Compose with policies for per-user filtering. Filter before constructing the list. Pruning nodes after the tree is built leaves you with orphaned depth values and children pointing at invisible parents.
3.0.4 trades an exception for silence. This is the one thing to internalise. Previously a bad depth — a misconfigured max_depth, an off-by-one in a recursive partial, an empty tree — threw at the call site and you found it in testing. Now it returns an empty list. Convenient in production, dangerous in development. Add an explicit assertion in your test suite that your "expected to be empty" cases are actually empty for the right reason:
$this->assertTrue($list->atDepth(2)->isEmpty());
$this->assertFalse($list->atDepth(1)->isEmpty()); // guards against a silently empty tree
Confirm the depth indexing. Depth is conventionally either 0- or 1-based, and the two conventions are indistinguishable on a tree that only ever has one branch. Log depth for a known three-level fixture once and write the number into a comment on your partial. Every off-by-one you hit afterwards will be easier to diagnose.
Watch for N+1 when walking children. Loading a list from Eloquent usually needs the children relation eager loaded, or you pay a query per node. On a wide menu this is invisible in development and obvious in production.
Never mutate a cached tree. If you are caching arrays, remember that toArray() deep-copies and the object does not — mutating a cached instance leaks changes across requests in long-running workers (Octane, queue jobs). Cache the array, rebuild the object per request.
Config caching bites in production. php artisan config:cache will happily serve a stale depth or ordering setting. If behaviour differs between php artisan serve and your deployed environment, this is the first thing to check.
Extension points: prefer subclassing ItemList or adding a custom renderer over patching vendor code — the traversal internals are exactly what just changed in a patch release, so anything reaching into them is fragile. If the package exposes macros or a collection-style API, use those.
Debugging a mangled tree: dump the flattened list with depths before dumping the nested structure. $list->flatten()->each(fn ($i) => dump($i->depth, $i->title)) makes parent/child mistakes obvious in a way a nested dd() does not.
Upgrade policy: 3.0.4 is a drop-in patch, but because it changes error behaviour rather than fixing a crash, run your suite after upgrading and look specifically for tests that expected an exception. If any existed, they will now pass for the wrong reason — or fail.
How can I help you explore Laravel packages today?