Managing navigation menus can become challenging in Laravel applications as they grow, especially with dynamic elements like role-based access controls. This blog post explores how to simplify and structure your menus using a Menu Builder system, making them easier to maintain, extend, and scale.
The Problem
In many Laravel projects, Blade templates handle menu visibility using conditionals:
@can('viewAdmin')
<a href="{{ route('administration.index') }}">
{{ __('Administration') }}
</a>
@endcan
While this approach works for simple applications, it becomes cluttered and unmanageable as the number of menus increases.
The Solution
A Menu Builder system encapsulates menu logic into reusable classes, improving:
- Maintainability: Centralised menu definitions.
- Scalability: Dynamically generating menus based on roles or permissions.
- Reusability: Sharing menus across views.
Support my mission to empower the developer community by sponsoring my work—your contributions help me build and share valuable tools, insights, and resources: Learn more here.
Step-by-Step Implementation
1. Define a Gate for viewAdmin
To control access to the administration menu, define a viewAdmin
gate in your AuthServiceProvider
:
use Illuminate\Support\Facades\Gate;
use App\Models\User;
class AuthServiceProvider extends ServiceProvider
{
public function boot()
{
$this->registerPolicies();
Gate::define('viewAdmin', function (User $user) {
return $user->hasRole('admin'); // Replace with your app's role-checking logic
});
}
}
2. Create the MenuItem
Class
The MenuItem
class defines all attributes of a menu item, such as label, URL, icon, and visibility:
<?php
namespace App\Actions\Builder;
use CleaniqueCoders\Traitify\Contracts\Builder;
use InvalidArgumentException;
class MenuItem implements Builder
{
private string $label;
private string $url;
private string $target = '_self';
private array $attributes = [];
private array $children = [];
private string $icon = 'o-squares-2x2';
private ?string $description = null;
private ?string $tooltip = null;
private $visible = true;
private array $output = [];
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
public function setUrl(string $url): self
{
$this->url = $url;
return $this;
}
public function setTarget(string $target): self
{
$this->target = $target;
return $this;
}
public function addAttribute(string $key, string $value): self
{
$this->attributes[$key] = $value;
return $this;
}
public function addChild(MenuItem $child): self
{
$this->children[] = $child;
return $this;
}
public function setIcon(string $icon): self
{
$this->icon = $icon;
return $this;
}
public function setDescription(string $description): self
{
$this->description = $description;
return $this;
}
public function setTooltip(string $tooltip): self
{
$this->tooltip = $tooltip;
return $this;
}
public function setVisible($visible): self
{
if (! is_bool($visible) && ! is_callable($visible)) {
throw new InvalidArgumentException('The visible property must be a boolean or a callable.');
}
$this->visible = $visible;
return $this;
}
public function isVisible(): bool
{
return is_callable($this->visible) ? call_user_func($this->visible) : $this->visible;
}
public function build(): self
{
$this->output = [
'label' => $this->label,
'url' => $this->url,
'target' => $this->target,
'attributes' => $this->attributes,
'icon' => $this->icon,
'description' => $this->description,
'tooltip' => $this->tooltip,
'children' => array_filter(
array_map(fn (MenuItem $child) => $child->build()->toArray(), $this->children),
fn (array $child) => ! empty($child)
),
];
return $this;
}
public function toArray(): array
{
return $this->output;
}
public function toJson(int $options = 0): string
{
return json_encode($this->toArray(), $options, 512);
}
}
3. Create the Menu
Builder
The Menu
builder resolves and constructs menus dynamically:
namespace App\Actions\Builder;
class Menu
{
public static function make()
{
return new self;
}
public function build(string $builder)
{
$class = match ($builder) {
'navbar' => Navbar::class,
'sidebar' => Sidebar::class,
'administration' => Administration::class,
default => Navbar::class,
};
$builder = new $class;
return $builder->build();
}
}
Access the menus using a helper function:
<?php
use App\Actions\Builder\Menu;
if (! function_exists('menu')) {
function menu(string $builder)
{
return Menu::make()->build($builder)->menus();
}
}
4. Administration Menu
Define administration-specific menu items in the Administration
class:
<?php
namespace App\Actions\Builder\Menu;
use App\Actions\Builder\MenuItem;
use CleaniqueCoders\Traitify\Contracts\Builder;
use CleaniqueCoders\Traitify\Contracts\Menu;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Gate;
class Administration implements Builder, Menu
{
private Collection $menus;
public function menus(): Collection
{
return $this->menus;
}
public function build(): self
{
$this->menus = collect([
(new MenuItem)
->setLabel(__('Issues'))
->setUrl(url(config('telescope.path')))
->setTarget('_blank')
->setVisible(fn () => Gate::allows('viewTelescope'))
->setTooltip(__('View Telescope issues'))
->setDescription(__('Access application issues using Laravel Telescope'))
->setIcon('o-bug'), // Heroicon outline for a bug
(new MenuItem)
->setLabel(__('Queues'))
->setUrl(url(config('horizon.path')))
->setTarget('_blank')
->setVisible(fn () => Gate::allows('viewHorizon'))
->setTooltip(__('Manage queues'))
->setDescription(__('Access Laravel Horizon to monitor and manage queues'))
->setIcon('o-cog'), // Heroicon outline for settings/tasks
(new MenuItem)
->setLabel(__('Access Control'))
->setUrl(route('security.access-control.index'))
->setVisible(fn () => Gate::allows('viewAccessControl'))
->setTooltip(__('Manage access control'))
->setDescription(__('Define and manage access control rules'))
->setIcon('o-lock-closed'),
(new MenuItem)
->setLabel(__('Users'))
->setUrl(route('security.users.index'))
->setVisible(fn () => Gate::allows('viewUser'))
->setTooltip(__('Manage users'))
->setDescription(__('View and manage user accounts'))
->setIcon('o-user-group'),
(new MenuItem)
->setLabel(__('Audit Trail'))
->setUrl(route('security.audit-trail.index'))
->setVisible(fn () => Gate::allows('viewAudit'))
->setTooltip(__('View audit trails'))
->setDescription(__('Audit logs for security and activity tracking'))
->setIcon('o-document-text'),
])->reject(fn (MenuItem $menu) => ! $menu->isVisible())
->map(fn (MenuItem $menu) => $menu->build()->toArray());
return $this;
}
}
5. Define Routes
Add the following route configuration for the administration page:
<?php
use Illuminate\Support\Facades\Route;
Route::middleware(['auth:sanctum', 'verified', 'can:viewAdmin'])
->as('administration.')
->prefix('administration')
->group(function () {
Route::view('/', 'administration.index')->name('index');
});
6. Usage in Blade Templates
Navigation Menu (navigation-menu.blade.php
):
@can('viewAdmin')
<a href="{{ route('administration.index') }}">
<x-icon name="o-computer-desktop" />
{{ __('Administration') }}
</a>
@endcan
Administration Menu (administration/index.blade.php
):
<x-app-layout>
<x-slot name="header">{{ __('Administration') }}</x-slot>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
@foreach (menu('administration') as $menu)
<div class="
p-4 border rounded shadow
hover:bg-gray-100
">
<x-icon :name="$menu['icon']" />
<a href="{{ $menu['url'] }}"
target="{{ $menu['target'] }}">
<span>{{ $menu['label'] }}</span>
</a>
<p>{{ $menu['description'] }}</p>
</div>
@endforeach
</div>
</x-app-layout>
Output
Here the final output that you can have:
Support my mission to empower the developer community by sponsoring my work—your contributions help me build and share valuable tools, insights, and resources: Learn more here.
Conclusion
This Menu Builder system simplifies navigation management in Laravel by:
- Centralising menu definitions for better maintainability.
- Dynamically controlling menu visibility using roles or permissions.
- Reusing menu logic across views and layouts.
By adopting this approach, you can scale your navigation system seamlessly, even in complex applications.
You may want to load your menu details from database and construct the menus that you want. But for me, this is good enough. I don't have projects require me to use database driven menu configuration.
The codes can be found here.
Try it out and share your thoughts! 🚀
Photo by LinedPhoto on Unsplash
Top comments (0)