terranet/menus
Terranet Menus is a Laravel package for building and managing database-backed navigation menus. Define menus and nested items, order them, link to routes or URLs, and render them in your Blade views.
Install the package via Composer and register the service provider (it predates package auto-discovery, so this step is manual in older setups):
composer require terranet/menus
Add the provider to config/app.php (or bootstrap/providers.php on Laravel 11+) if auto-discovery does not pick it up:
Terranet\Menus\Providers\MenuServiceProvider::class,
Publish and run the migrations, plus any config file that ships with the package:
php artisan vendor:publish --provider="Terranet\Menus\Providers\MenuServiceProvider"
php artisan migrate
Where to look first: open the published migration to understand the schema — this tells you the real column names (title, url, parent_id, order/position, target, active) far faster than the README, which is thin. Then read the Menu model for scopes, accessors, and any tree helpers.
First use case: a database-driven navigation bar. Create a couple of root items and a child, then render them.
use Terranet\Menus\Models\Menu;
Menu::create(['title' => 'Home', 'url' => '/', 'parent_id' => null, 'order' => 1]);
Menu::create(['title' => 'About', 'url' => '/about', 'parent_id' => null, 'order' => 2]);
Menu::create(['title' => 'Team', 'url' => '/about/team', 'parent_id' => 2, 'order' => 1]);
Render in a Blade partial by iterating the tree once you've loaded it:
@foreach ($menus as $item)
<a href="{{ url($item->url) }}">{{ $item->title }}</a>
@if ($item->children->isNotEmpty())
@foreach ($item->children as $child)
<a href="{{ url($child->url) }}">{{ $child->title }}</a>
@endforeach
@endif
@endforeach
Load the tree in one query, not N. The single biggest day-to-day pattern with a parent_id-based menu is avoiding recursive queries. Fetch all rows once, group by parent_id, and build the hierarchy in memory:
$flat = Menu::orderBy('order')->get()->groupBy('parent_id');
$build = function ($parentId) use (&$build, $flat) {
return collect($flat->get($parentId, []))->map(function ($item) use (&$build) {
$item->setRelation('children', $build($item->id));
return $item;
})->values();
};
$nav = $build(null);
If the package already defines children() / parent() relations, use them with with('children'), but only for known-depth menus — deep eager loading multiplies queries.
Cache the rendered tree. Menus change rarely and render on every request. Cache the built structure (not the HTML, so you can vary markup per layout) and key it by a version you bump on save:
$nav = Cache::rememberForever('menu.main', fn () => $build(null));
Add a model observer on saved/deleted to Cache::forget('menu.main').
Keep content and presentation separate. Store only title, url, parent_id, order, target. Do not bake in icons, CSS classes, or permissions as new columns — use a convention table or a meta JSON column the package likely supports, so a designer can restyle without a migration.
Programmatic vs. admin-driven menus. For small static sites, seed menus in a MenuSeeder and rebuild the cache in php artisan db:seed. For CMS-style sites, wire the package's model into your own CRUD resource (Filament, Nova, or hand-rolled Blade). The package gives you the model, not the UI — do not expect an admin screen out of the box.
Route integration. Prefer storing route names or slugs over full URLs. A url accessor that calls route($this->route_name) keeps links working across environments where the host differs.
Blade partial per depth. Recursively include a partial for dropdowns instead of nesting foreach three levels deep — it stays readable when the menu grows.
$dates, old Str/Arr helpers, and ServiceProvider::provides() mismatches. Add the provider manually if auto-discovery fails silently.Class "Terranet\Menus\..." not found or a missing table because migrations never ran.order is a reserved word in MySQL. If the column is literally order, always qualify it (menu_items.order) in raw queries or it will parse as ORDER BY.vendor/terranet/menus/src — you will not find Stack Overflow threads. Log the SQL with DB::listen to confirm the queries the relations generate.$menu->children inside a Blade loop without eager loading will fire one query per row. Confirm with a query counter (Laravel Debugbar or a DB::listen count) before shipping.parent_id changes on a single row invalidate the whole tree — forget the entire menu key, not one item's key.url values, validate them (allow only /-relative paths or http(s)), otherwise you get stored javascript: links. Escape titles with {{ }} — never {!! !!} unless you control the input.parent_id schema prevents setting an item's parent to its own descendant, which will hang your in-memory tree builder. Guard on save in the controller/observer.config binding or extend it to add scopes like scopeActive and accessors like getDepthAttribute() rather than editing vendor files.How can I help you explore Laravel packages today?