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

Tomato Menus Module Laravel Package

tomatophp/tomato-menus-module

A Tomato PHP module for Laravel that adds menu management to your app, letting you create, organize, and render navigation menus with a flexible builder and easy integration into Tomato admin panels and frontends.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the module and let Laravel's package discovery register its service provider:

composer require tomatophp/tomato-menus-module
php artisan vendor:publish --tag="tomato-menus-module-config"
php artisan vendor:publish --tag="tomato-menus-module-migrations"
php artisan migrate

If you are running the wider Tomato admin stack, the module expects tomato-admin (or the Filament plugin variant) to be installed and its auth guard configured first — install order matters, because the module's routes and resources register against the panel that is already booted.

Look here first, in this order:

  1. vendor/tomatophp/tomato-menus-module/database/migrations/ — the schema tells you more about the package than the README. You want to see the menus and menu_items tables: the key/slug column on the parent and the parent_id, type, order columns on the child.
  2. src/Models/Menu.php and src/Models/MenuItem.php — relationships (items, children), scopes, casts, and any HasTranslations trait usage.
  3. config/tomato-menus.php — cache toggle, route prefix, middleware, and the item-type map.
  4. resources/lang/ and resources/views/ — the renderables and the strings the module ships.

First use case — a header menu you can actually ship:

Create a menu record with a stable key (header, footer, sidebar), attach items in the admin UI, then render it from your layout. Resist hardcoding a menu ID anywhere: resolve by key so the menu survives DB refreshes between environments.

{{-- resources/views/layouts/app.blade.php --}}
@include('tomato-menus::render', ['menu' => menus('header')])

or, if you prefer to own the markup, fetch the tree once and build a recursive partial over $menu->items filtered to parent_id === null.

Implementation Patterns

Two-table adjacency list. The package models menus as a named container plus a self-referencing item tree. Treat parent_id + order as the source of truth for structure; never derive order from id or created_at, because drag-and-drop reordering rewrites order in bulk.

Resolve links late, not at save time. Store the reference (type = route / url / page / morph model + id), and resolve the actual href at render time. If you store a resolved URL, every slug change on the target page silently leaves a dead link in the menu. A small resolver keeps this honest:

public function resolveUrl(): string
{
    return match ($this->type) {
        'route' => Route::has($this->route) ? route($this->route, $this->params ?? []) : '#',
        'model' => optional($this->model)->url() ?? '#',
        default => $this->url ?: '#',
    };
}

Cache the rendered tree, keyed by menu key. Menus are read on every page and written once a month — the ideal cache candidate. Wrap the tree build in Cache::remember("menu:{$key}", $ttl, ...) and flush from model events (saved, deleted) on both Menu and MenuItem.

Blade component + view composer. For multi-menu layouts, a view composer on layouts.* that shares $headerMenu, $footerMenu keeps controllers clean and guarantees the tree is built once per request, not per @include.

Seed menus in a seeder, manage items in the UI. Ship MenuSeeder that calls firstOrCreate(['key' => 'header']). Items are editorial content; don't seed them outside local/demo environments, or a deploy will resurrect items an editor deliberately deleted.

Custom item types via config. Most Tomato modules expose a type map in config/tomato-menus.php. Register your own type class there rather than forking vendor views — that is the supported extension point, and it survives composer update.

Testing. Use RefreshDatabase, build a three-level tree in a factory, and assert flattened order plus resolved URLs. Menu bugs are almost always ordering or nesting bugs, and they are cheap to pin down in a test:

$menu->items()->saveMany([
    MenuItem::factory()->make(['order' => 1]),
    $child = MenuItem::factory()->make(['order' => 2, 'parent_id' => 1]),
]);
$this->assertSame([1, 2], $menu->fresh()->flattenedItems()->pluck('order')->all());

Gotchas and Tips

v1.0.2 is a translation-only release. The single change is a lang-file update from a first-time contributor. Nothing in it touches schema, routes, or public APIs, so it is safe to bump. But: if you have published the module's lang files (vendor:publish --tag=...translations), Laravel now loads your copy, not vendor's — you will not see any of the new strings until you re-publish or manually diff vendor/tomatophp/tomato-menus-module/resources/lang against lang/vendor/tomato-menus-module. This class of release is exactly where stale published assets go unnoticed for months.

Treat published lang files as a tracked fork. Publishing translations flips the module from "upgrade and get fixes" to "merge by hand forever." If you only need one or two keys overridden, prefer a small project-level lang file over publishing the whole set.

Bulk reorder may skip model events. Drag-and-drop reordering frequently persists via a query builder loop (MenuItem::where(...)->update(['order' => $i])), which does not fire saved. Cache invalidation hooked only to model events will leave a stale tree. Flush the menu cache explicitly at the end of the reorder handler.

N+1 in recursive rendering. Rendering $item->children inside a partial triggers a query per level. Eager-load the whole subtree in one go (->with('childrenRecursive') or build the tree from a single flat query) and pass the assembled array to the view.

parent_id deletion semantics. Check the migration — if the FK is restrictive, deleting a parent with children throws; if it cascades, deleting a parent silently wipes its subtree. Neither is obviously right; decide per project and enforce it in the admin policy.

Route caching breaks dynamic links. If menu items store route names with parameters, php artisan route:cache will throw on missing params at render time. Always guard resolution with Route::has() and a try/catch, falling back to # rather than 500-ing the whole layout.

Locale fallback shows blanks, not fallbacks. With spatie/laravel-translatable, an empty string is a valid translation and will not fall back to the default locale. Editors who "clear" a field to retranslate produce empty menu labels. Normalise '' to null on save.

Route/prefix collisions. The module registers admin routes under a configurable prefix. If your app already owns that prefix (or you run two Tomato modules with clashing route names), you get silent 404s on the admin screen only — check php artisan route:list --path=menus first when the UI misbehaves.

Pin to the Tomato core version you actually run. These modules move in lockstep with tomato-admin/Filament. A patch bump to the module is low-risk, but a minor bump can require a matching core version — read the composer.json constraints, not just the changelog.

Debugging checklist: Menu::with('items')->where('key','header')->first()->toArray() to confirm structure; DB::listen() to catch the N+1; Cache::forget("menu:header") when the UI and page disagree; and php artisan vendor:publish --force only after committing, since it overwrites your customised views.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
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