Admin Menu System
Complete guide to adding, customizing, and managing admin sidebar menus in LaraDashboard using the AdminMenuService.
Admin Menu System
LaraDashboard provides a flexible, permission-aware admin menu system that supports groups, submenus, icons, and priority-based ordering. Modules can easily register their own menus using hooks.
Architecture Overview
The admin menu system consists of three main components:
| Component | Location | Purpose |
|---|---|---|
AdminMenuService |
app/Services/MenuService/AdminMenuService.php |
Manages menu registration and rendering |
AdminMenuItem |
app/Services/MenuService/AdminMenuItem.php |
Menu item data structure |
AdminFilterHook |
app/Enums/Hooks/AdminFilterHook.php |
Hook definitions for menu customization |
┌─────────────────────────────────────────────────────────────┐
│ Admin Sidebar │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Group: "Main" │ │
│ │ ├── Dashboard (priority: 1) │ │
│ │ ├── Posts (priority: 10) │ │
│ │ │ ├── All Posts │ │
│ │ │ ├── Add New │ │
│ │ │ └── Categories │ │
│ │ ├── CRM (priority: 21) [From Module] │ │
│ │ │ ├── Dashboard │ │
│ │ │ ├── Contacts │ │
│ │ │ └── ... │ │
│ │ └── Media Library (priority: 35) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Group: "More" │ │
│ │ ├── Modules (priority: 25) │ │
│ │ ├── Access Control (priority: 30) │ │
│ │ ├── Settings (priority: 40) │ │
│ │ └── Logout (priority: 10000) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
AdminMenuItem Properties
The AdminMenuItem class represents a single menu item with these properties:
| Property | Type | Default | Description |
|---|---|---|---|
label |
string | '' |
Display text for the menu item |
title |
string | '' |
Alternative to label (uses label if empty) |
icon |
string | null |
Iconify icon identifier (e.g., lucide:home) |
iconClass |
string | null |
Custom icon CSS class |
route |
string | null |
URL/route for the menu item |
active |
boolean | false |
Whether menu item is currently active |
id |
string | null |
Unique identifier for the menu item |
children |
array | [] |
Child menu items for submenus |
target |
string | null |
Link target (e.g., _blank) |
priority |
int | 1 |
Sort order (lower = higher position) |
permissions |
array | [] |
Required permissions (any match grants access) |
itemStyles |
string | '' |
Custom inline CSS styles |
htmlData |
string | null |
Raw HTML content (replaces default rendering) |
Adding Menus from Modules
Step 1: Create a Menu Service
Create a dedicated service class to manage your module's menu:
<?php
declare(strict_types=1);
namespace Modules\YourModule\Services;
use App\Services\MenuService\AdminMenuItem;
use Illuminate\Support\Facades\Route;
class YourModuleMenuService
{
/**
* Add your module's menu to the admin sidebar.
* This method is called via hook filter.
*
* @param array $groups Current menu groups
* @return array Modified menu groups
*/
public function addMenu(array $groups): array
{
$menuItem = $this->getMenu();
// Add to "Main" group (or create your own group)
$groups[__('Main')][] = $menuItem;
return $groups;
}
/**
* Build the menu structure for your module.
*/
public function getMenu(): AdminMenuItem
{
return (new AdminMenuItem())->setAttributes([
'label' => __('Your Module'),
'icon' => 'lucide:box',
'route' => route('admin.your-module.index'),
'active' => Route::is('admin.your-module.*'),
'id' => 'your-module',
'priority' => 25,
'permissions' => ['your-module.view'],
'children' => $this->getMenuChildren(),
]);
}
/**
* Get child menu items.
*/
protected function getMenuChildren(): array
{
return [
(new AdminMenuItem())->setAttributes([
'label' => __('Dashboard'),
'route' => route('admin.your-module.dashboard'),
'active' => Route::is('admin.your-module.dashboard'),
'priority' => 1,
'permissions' => ['your-module.view'],
]),
(new AdminMenuItem())->setAttributes([
'label' => __('Items'),
'route' => route('admin.your-module.items.index'),
'active' => Route::is('admin.your-module.items.*'),
'priority' => 10,
'permissions' => ['your-module.view'],
]),
(new AdminMenuItem())->setAttributes([
'label' => __('Settings'),
'route' => route('admin.your-module.settings'),
'active' => Route::is('admin.your-module.settings'),
'priority' => 100,
'permissions' => ['your-module.settings'],
]),
];
}
}
Step 2: Register the Menu via Hook
In your module's main service class, register the menu using the ADMIN_MENU_GROUPS_BEFORE_SORTING hook:
<?php
declare(strict_types=1);
namespace Modules\YourModule\Services;
use App\Enums\Hooks\AdminFilterHook;
use App\Support\Facades\Hook;
class YourModuleService
{
public function __construct(
private readonly YourModuleMenuService $menuService
) {
}
/**
* Bootstrap your module.
* Called from ServiceProvider boot method.
*/
public function bootstrap(): void
{
// Register admin menu
Hook::addFilter(
AdminFilterHook::ADMIN_MENU_GROUPS_BEFORE_SORTING,
[$this->menuService, 'addMenu']
);
}
}
Step 3: Call Bootstrap in ServiceProvider
In your module's service provider:
<?php
declare(strict_types=1);
namespace Modules\YourModule\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\YourModule\Services\YourModuleMenuService;
use Modules\YourModule\Services\YourModuleService;
class YourModuleServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Bootstrap after app is ready
$this->app->booted(function () {
$this->app->make(YourModuleService::class)->bootstrap();
});
}
public function register(): void
{
// Register as singletons for performance
$this->app->singleton(YourModuleMenuService::class);
$this->app->singleton(YourModuleService::class);
}
}
Menu Configuration Options
Simple Menu Item (No Children)
$menuItem = (new AdminMenuItem())->setAttributes([
'label' => __('Reports'),
'icon' => 'lucide:bar-chart',
'route' => route('admin.reports.index'),
'active' => Route::is('admin.reports.*'),
'id' => 'reports',
'priority' => 30,
'permissions' => ['reports.view'],
]);
Menu with Submenu
$menuItem = (new AdminMenuItem())->setAttributes([
'label' => __('Settings'),
'icon' => 'lucide:settings',
'id' => 'settings',
'active' => Route::is('admin.settings.*'),
'priority' => 40,
'permissions' => ['settings.view', 'settings.edit'],
'children' => [
(new AdminMenuItem())->setAttributes([
'label' => __('General'),
'route' => route('admin.settings.general'),
'active' => Route::is('admin.settings.general'),
'priority' => 10,
]),
(new AdminMenuItem())->setAttributes([
'label' => __('Email'),
'route' => route('admin.settings.email'),
'active' => Route::is('admin.settings.email'),
'priority' => 20,
]),
],
]);
External Link (Opens in New Tab)
$menuItem = (new AdminMenuItem())->setAttributes([
'label' => __('Documentation'),
'icon' => 'lucide:external-link',
'route' => 'https://docs.example.com',
'target' => '_blank',
'id' => 'docs',
'priority' => 50,
]);
Custom HTML Menu Item
$menuItem = (new AdminMenuItem())->setAttributes([
'label' => __('Logout'),
'id' => 'logout',
'priority' => 10000,
'html' => '
<li>
<form method="POST" action="' . route('logout') . '">
' . csrf_field() . '
<button type="submit" class="menu-item group w-full text-left">
<iconify-icon icon="lucide:log-out" class="menu-item-icon"></iconify-icon>
<span class="menu-item-text">' . __('Logout') . '</span>
</button>
</form>
</li>
',
]);
Route with Parameters
// Using array configuration
$menuItem = [
'label' => __('Category Posts'),
'route' => 'admin.posts.index', // Route name
'params' => 'blog', // Route parameter
'active' => request()->is('admin/posts/blog*'),
'priority' => 15,
];
// Or with multiple params
$menuItem = [
'label' => __('Edit User'),
'route' => 'admin.users.edit',
'params' => ['user' => $userId],
'active' => Route::is('admin.users.edit'),
];
Menu Groups
Menus are organized into groups displayed as sections in the sidebar:
Default Groups
| Group | Purpose |
|---|---|
Main |
Primary navigation items |
More |
Secondary/administrative items |
Adding to a Specific Group
public function addMenu(array $groups): array
{
// Add to "Main" group
$groups[__('Main')][] = $this->getMainMenu();
// Add to "More" group
$groups[__('More')][] = $this->getSettingsMenu();
// Create custom group
$groups[__('Custom Section')][] = $this->getCustomMenu();
return $groups;
}
Group Display Order
Groups are displayed in the order they are added. The core AdminMenuService adds groups in this order:
Main- Dashboard, Post Types, MediaMore- Modules, Access Control, Settings, Monitoring, Logout
Priority System
Menu items are sorted by priority within each group:
| Priority Range | Typical Usage |
|---|---|
| 1-10 | Top-level items (Dashboard) |
| 10-30 | Primary content (Posts, CRM) |
| 30-50 | Secondary features (Media, Reports) |
| 50-100 | Administrative (Settings) |
| 100+ | Bottom items (Logout) |
// Dashboard at top
['label' => 'Dashboard', 'priority' => 1]
// Content in middle
['label' => 'Posts', 'priority' => 10]
['label' => 'CRM', 'priority' => 21]
// Settings near bottom
['label' => 'Settings', 'priority' => 40]
// Logout at very bottom
['label' => 'Logout', 'priority' => 10000]
Permission Handling
Menu items automatically hide when users lack required permissions:
Single Permission
$menuItem = (new AdminMenuItem())->setAttributes([
'label' => __('Users'),
'permissions' => 'user.view', // Can be string
]);
Multiple Permissions (Any Match)
$menuItem = (new AdminMenuItem())->setAttributes([
'label' => __('Access Control'),
'permissions' => ['user.view', 'role.view', 'permission.view'],
// Shows if user has ANY of these permissions
]);
Permission on Children
$menuItem = (new AdminMenuItem())->setAttributes([
'label' => __('CRM'),
'permissions' => ['contact.view'], // Parent requires this
'children' => [
(new AdminMenuItem())->setAttributes([
'label' => __('Contacts'),
'permissions' => ['contact.view'], // Each child can have own
]),
(new AdminMenuItem())->setAttributes([
'label' => __('Settings'),
'permissions' => ['crm.settings'], // Different permission
]),
],
]);
Available Hooks
Filter Hooks
| Hook | Purpose |
|---|---|
ADMIN_MENU_GROUPS_BEFORE_SORTING |
Add/modify menu groups before sorting |
SIDEBAR_MENU_{group} |
Modify items in specific group |
SIDEBAR_MENU_BEFORE_{id} |
Add content before specific menu item |
SIDEBAR_MENU_AFTER_{id} |
Add content after specific menu item |
SIDEBAR_MENU_ITEM_AFTER_{id} |
Add content after menu item renders |
Using Hooks to Modify Menus
use App\Enums\Hooks\AdminFilterHook;
use App\Support\Facades\Hook;
// Add menu items
Hook::addFilter(
AdminFilterHook::ADMIN_MENU_GROUPS_BEFORE_SORTING,
function (array $groups) {
$groups[__('Main')][] = (new AdminMenuItem())->setAttributes([
'label' => 'Custom Item',
'route' => route('admin.custom'),
'priority' => 15,
]);
return $groups;
}
);
// Modify specific group
Hook::addFilter(
AdminFilterHook::SIDEBAR_MENU->value . 'main',
function (array $items) {
// Filter or modify items in Main group
return array_filter($items, fn($item) => $item->id !== 'unwanted');
}
);
// Add content before a menu item
Hook::addFilter(
AdminFilterHook::SIDEBAR_MENU_BEFORE->value . 'settings',
function (string $html) {
return $html . '<li class="menu-divider"></li>';
}
);
Real-World Example: CRM Module
Here's how the CRM module implements its menu:
CrmMenuService.php
<?php
declare(strict_types=1);
namespace Modules\Crm\Services;
use App\Services\MenuService\AdminMenuItem;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
class CrmMenuService
{
public function addMenu(array $groups): array
{
$groups[__('Main')][] = $this->getMenu();
return $groups;
}
public function getMenu(): AdminMenuItem
{
return (new AdminMenuItem())->setAttributes([
'label' => __('CRM'),
'icon' => 'lucide:headphones',
'route' => route('admin.crm.dashboard'),
'active' => Route::is('admin.crm.*'),
'id' => 'crm',
'priority' => 21,
'permissions' => ['contact.view'],
'children' => $this->getMenuChildren(),
]);
}
protected function getMenuChildren(): array
{
return [
(new AdminMenuItem())->setAttributes([
'label' => __('Dashboard'),
'route' => route('admin.crm.dashboard'),
'active' => Route::is('admin.crm.dashboard'),
'priority' => 1,
'permissions' => ['crm.dashboard'],
]),
(new AdminMenuItem())->setAttributes([
'label' => __('Contacts'),
'route' => route('admin.crm.contacts.index'),
'active' => Route::is('admin.crm.contacts.*'),
'priority' => 10,
'permissions' => ['contact.view'],
]),
(new AdminMenuItem())->setAttributes([
'label' => __('Deals'),
'route' => route('admin.crm.deals.index'),
'active' => Route::is('admin.crm.deals.*'),
'priority' => 40,
'permissions' => ['contact.view'],
]),
// ... more children
];
}
}
CrmService.php (Bootstrap)
<?php
declare(strict_types=1);
namespace Modules\Crm\Services;
use App\Enums\Hooks\AdminFilterHook;
use App\Support\Facades\Hook;
class CrmService
{
public function __construct(
private readonly CrmMenuService $crmMenuService
) {
}
public function bootstrap(): void
{
// Register admin menu via hook
Hook::addFilter(
AdminFilterHook::ADMIN_MENU_GROUPS_BEFORE_SORTING,
[$this->crmMenuService, 'addMenu']
);
}
}
Icon Reference
Use Iconify icons (Lucide, Feather, etc.):
| Icon | Identifier | Common Use |
|---|---|---|
| Dashboard | lucide:layout-dashboard |
Dashboard pages |
| Users | lucide:users |
User management |
| Settings | lucide:settings |
Settings pages |
| Box | lucide:box |
Modules/packages |
| File | lucide:file-text |
Posts/content |
| Image | lucide:image |
Media |
| Key | lucide:key |
Access control |
| Chart | lucide:bar-chart |
Reports/analytics |
lucide:mail |
Email features | |
| Phone | lucide:headphones |
CRM/support |
| Shopping | lucide:shopping-cart |
E-commerce |
Best Practices
- Use descriptive IDs - Helps with hooks and debugging
- Set appropriate priorities - Keep menus logically ordered
- Check permissions - Always specify required permissions
- Use translations - Wrap labels in
__()for i18n - Group related items - Use submenus for related pages
- Keep menus minimal - Don't overwhelm users with options
- Active state accuracy - Ensure
activecorrectly highlights current page - Singleton services - Register menu services as singletons for performance
Troubleshooting
Menu Not Appearing
- Check that your module is enabled
- Verify the hook is registered in
bootstrap()method - Ensure the
bootstrap()is called in$this->app->booted()callback - Check user has required permissions
- Verify route names exist
Menu in Wrong Position
- Adjust the
priorityvalue - Lower priority = higher position
- Check if other modules use similar priorities
Submenu Not Expanding
- Verify
activestate is correctly set - Check child
activestates - Ensure
Route::is()pattern matches current route
Next Steps
- Hooks Reference - Learn about all available hooks
- Module Development - Complete module development guide
- Permissions - Understanding permissions