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:

  1. Main - Dashboard, Post Types, Media
  2. More - 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
Mail lucide:mail Email features
Phone lucide:headphones CRM/support
Shopping lucide:shopping-cart E-commerce

Best Practices

  1. Use descriptive IDs - Helps with hooks and debugging
  2. Set appropriate priorities - Keep menus logically ordered
  3. Check permissions - Always specify required permissions
  4. Use translations - Wrap labels in __() for i18n
  5. Group related items - Use submenus for related pages
  6. Keep menus minimal - Don't overwhelm users with options
  7. Active state accuracy - Ensure active correctly highlights current page
  8. Singleton services - Register menu services as singletons for performance

Troubleshooting

Menu Not Appearing

  1. Check that your module is enabled
  2. Verify the hook is registered in bootstrap() method
  3. Ensure the bootstrap() is called in $this->app->booted() callback
  4. Check user has required permissions
  5. Verify route names exist

Menu in Wrong Position

  1. Adjust the priority value
  2. Lower priority = higher position
  3. Check if other modules use similar priorities

Submenu Not Expanding

  1. Verify active state is correctly set
  2. Check child active states
  3. Ensure Route::is() pattern matches current route

Next Steps

/