laravel-enso/menus
Database-driven menu management for Laravel Enso: define and organize application menus, render hierarchical navigation, and control item visibility through the Enso permission system. Integrates cleanly with Enso's core modules and UI components.
The menus package is a management layer for Laravel Enso's main application menu — it gives you a database-backed menu tree plus a Vue-based admin UI to add, remove, reorder, and nest menu entries without touching code.
Minimal steps:
laravel-enso/enso core is installed):
composer require laravel-enso/menus
php artisan vendor:publish --tag=menus-migrations
php artisan migrate
If the package ships seeders (default root menu, dashboard entry, etc.), run php artisan db:seed --class=MenuSeeder or the Enso equivalent.routes/ inside the vendor folder or php artisan route:list | grep menu to see the CRUD endpoints it exposes./admin/menus on a stock Enso install). That screen is the primary day-to-day surface.First use case: add a top-level nav entry for a module you just built — give it a name, an icon (Font Awesome class used by Enso), a link/route, and a sort order. Then reorder it by drag-and-drop in the UI and confirm it persists across reloads.
Where to look first in the repo:
src/ — models, controllers, resources, policies.database/migrations/ — the schema (columns like name, icon, link, parent_id, order, is_active are the usual suspects; verify against the migration).resources/js/ — the Vue components driving the drag-and-drop / form behavior.README.md — for the exact publish tags and any post-install Artisan commands.Treat the UI as the source of truth, not seeders. Enso's menu is designed to be edited at runtime. Seed only the bootstrap tree (dashboard, system settings) and let admins manage everything else through the UI. If you hard-code menu items in seeders, they'll drift from the live structure and any future migration of the menu table will clobber hand edits.
Programmatic creation for module installers. When you write an Enso module that should register its own menu entry, create it in the module's service provider or seeder rather than documenting "go add a menu item":
Menu::create([
'name' => 'Reports',
'icon' => 'fa-file-alt',
'link' => '/reports',
'parent_id' => null,
'order' => Menu::max('order') + 1,
]);
Keep this idempotent (firstOrCreate on link or name) so re-running the seeder is safe.
Gate menu visibility by permission. Enso's menu items are typically shown conditionally based on the current user's permissions (usually a permission column or a relationship to Enso's roles/permissions tables — check the model). Wire your new menu entry to the same permission your route/controller uses; otherwise you'll show links that 403.
Build the tree once per request. If you need the menu outside the Enso frontend (e.g., a Blade layout or an API for a SPA), expose it through a small service that loads all rows and assembles the parent/child tree in one query, rather than lazy-loading children recursively. A flat get()->groupBy('parent_id') avoids N+1.
Cache the rendered tree, bust on write. Menu reads happen on every page load; writes happen rarely. Cache the built array with a key like enso.menus.tree.{$userId} or permission-agnostic enso.menus.tree plus a per-user filter at render time. Clear the cache in an observer on the Menu model (saved, deleted) or in the package's controller hooks.
Frontend integration. If you're extending the Vue side, reuse the package's components for the menu editor rather than writing your own drag-and-drop; the ordering logic and persistence format are already wired. For the actual navigation rendering, feed the tree into Enso's sidebar component via the existing API endpoint.
Testing. Factories aren't always shipped; define a local MenuFactory for feature tests and assert on ordering after a simulated reorder request. Test the permission gate explicitly — that's where most menu bugs live.
order and parent_id are the fragile fields. Reordering via drag-and-drop rewrites order values; if you also edit them manually in seeders you'll get duplicate/gapped ordering. Always append with max('order') + 1 and never assume contiguous integers.onDelete('cascade') or restrict. If children aren't cascaded, either soft-delete the subtree or reparent children to the grandparent in a deleting observer.CacheService) and whether it invalidates on save; if not, add an observer.__() conventions. If you need i18n, store a translation key rather than a literal label; otherwise the label is English-only in every locale.route('reports.index') and /reports works until you rename a route; route() strings break silently if routes are cached but the name changed.php artisan route:cache will hide broken link values because the URL never gets validated — sanity-check menu links after deploying route-cache changes.composer.json and verify the migration columns directly in the repo before relying on field names above.How can I help you explore Laravel packages today?