akaunting/menu
A Laravel package for building, managing, and rendering application menus. Define menu items and nested navigation structures in code, then output ready-to-use menus with flexible configuration for admin interfaces.
composer require akaunting/menu
Laravel auto-discovery registers Akaunting\Menu\MenuServiceProvider and the Menu facade alias on 5.5+. Publish the config so you can see the preset map:
php artisan vendor:publish --provider="Akaunting\Menu\MenuServiceProvider" --tag="menu"
Where to look first — the surface is small enough to read in one sitting:
src/Menu.php — the named-menu registry (create, get, exists, render)src/Builder.php — the menu object itself (addItem, addDivider, addHeader, addSubMenu)src/Item.php — links, icons, attributes, children, active stateresources/views/ — the preset Blade templates that render() emitsconfig/menu.php — preset names (navbar, sidebar, dropdown)First use case — a sidebar with a divider and a section header:
use Akaunting\Menu\Menu;
Menu::create('admin', function ($menu) {
$menu->addItem('Dashboard', route('dashboard'), 'home');
$menu->addItem('Reports', route('reports.index'), 'chart-bar');
$menu->addDivider();
$menu->addHeader('Settings');
$menu->addItem('Users', route('users.index'), 'user');
});
Render in your layout:
{!! Menu::get('admin')->preset('sidebar')->render() !!}
{{-- or, if the registry proxy exists in your build: --}}
{!! Menu::render('admin') !!}
Confirm exact method names (
makevscreate,addItemvsadd) against the README for the tagged version you install — the package was forked fromlavary/laravel-menuand the vocabulary differs slightly across forks.
Create the base menu in a service provider. Add auth- or tenant-dependent items in middleware or a view composer, not in boot() — under Octane or queue-driven rendering, the provider can run before a user is bound.
// app/Providers/MenuServiceProvider.php
public function boot(): void
{
if (! Menu::exists('sidebar')) {
Menu::create('sidebar', function ($menu) {
$menu->addItem(__('Dashboard'), route('dashboard'), 'home');
});
}
}
// app/Http/Middleware/BuildMenu.php
public function handle($request, Closure $next)
{
if (Menu::exists('sidebar') && $request->user()?->can('viewAny', User::class)) {
Menu::get('sidebar')->addItem(__('Users'), route('users.index'), 'users');
}
return $next($request);
}
Prefer omitting the item over rendering a greyed-out one. If your build exposes Item::authorize($ability, $arguments), use it — the check travels with the definition and the renderer drops it silently. Otherwise guard at registration time with Gate::allows().
$menu->addItem('Settings', '#', 'cog')
->addSubMenu('Users', route('users.index'), 'users')
->addSubMenu('Roles', route('roles.index'), 'shield');
addItemBefore / addItemAfter reference siblings by title string, so the ordering API is only as stable as your labels. If the menu is multilingual, either register in deterministic code order or add explicit data('order', n) and sort inside a custom preset.
Item::isActive() compares the current request against the item's route() / url(). Use route() whenever possible — string matching against url() breaks behind sub-directory installs, proxies, and query strings. Wildcard route matching (->route('reports.*')) is the sane choice for section-level items; otherwise set ->active() / ->inactive() explicitly for edge cases like detail pages.
Drop a Blade view under resources/views/vendor/menu/ and register it in config/menu.php:
'presets' => [
'sidebar' => 'menu::presets.sidebar',
'tiles' => 'menu::presets.tiles',
],
Then $menu->preset('tiles')->render(). This is the cleanest seam for diverging visually without touching vendor code — presets are just views, so you own everything inside them.
Build the menu server-side and ship data, not HTML: iterate items into a plain array and pass it as a prop. Don't reuse the Blade presets on the client — they carry active-state markup that will be stale.
In a module/plugin architecture, each module's provider registers its own items and never assumes the parent exists:
if (Menu::exists('sidebar')) {
Menu::get('sidebar')->addItemBefore('Users', 'Audit Log', route('audit.index'), 'file-text');
}
This keeps provider load order irrelevant, which matters once package discovery is in play.
Duplicate creation is the #1 crash. Calling Menu::create('sidebar', …) twice in a request throws (or silently resets, depending on version). Always guard with Menu::exists() or funnel creation through exactly one provider.
Don't escape the output. {!! !!}, never {{ }}. Conversely, don't call render() twice on the same builder — some presets mutate item attributes during rendering.
Caching HTML freezes active state and role filtering. If you cache, key on user_id (or role set) plus request()->path(). Caching the menu definition is only safe when nothing depends on auth.
Route resolution fails loudly. route('reports.index') inside a provider throws if the route isn't registered yet — and that takes down every page that renders the menu, not just the Reports page. Wrap optional-module routes in Route::has(...).
Icon names are passthrough strings. The package ships no icon set; Akaunting passes Font Awesome names. If your preset doesn't render the icon attribute, you're storing dead data and wondering why the sidebar looks bare.
Dividers and headers aren't links. Some presets emit <a> for every child; a leading addDivider() produces an orphan rule. They also count toward isEmpty()/count(), so a menu with only a header will render as non-empty.
Ordering by title is brittle. A single translation change to a label silently breaks addItemBefore. Keep labels stable, or move ordering into data() + a custom preset.
Facade collision. If lavary/laravel-menu is also installed, both register a Menu alias and you'll get confusing behavior. Remove one, or alias the other in config/app.php.
Maintenance posture. Last release October 2023, 37 stars. Stable but quiet — pin the version in composer.json. Bug fixes are realistically your fork, which is fine given the codebase is a few thousand lines.
Extension points worth knowing.
Menu::macro('admin', fn () => Menu::get('menu.admin')) for a shorter call site.Builder/Item subclass in the container if you need extra item properties — though data('key', 'value') usually covers it.Debug in tinker, not the browser. Menu::get('sidebar')->toArray() (or iterating ->items) shows exactly what was registered and which item is flagged active — far faster than reading rendered HTML.
Testing strategy. Assert on registration for logic (menu contains "Users" for an authorized user, not for a guest) and keep one snapshot per preset to catch markup changes on upgrade. Avoid asserting on active classes via full-page HTTP tests; the URL matching is the brittle part and will make the suite noisy.
How can I help you explore Laravel packages today?