Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Menu Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. 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.
  2. Publish config if the package ships one: php artisan vendor:publish --tag=<package>-config, then php artisan config:clear.
  3. Read the 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()
    );
}

Implementation Patterns

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.

Gotchas and Tips

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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager