MenuManager en Laravel

Aprende a construir un sistema de menús dinámicos en Laravel 12 desde cero. Guía paso a paso con código y ejemplos prácticos para aplicaciones web escalables

Introducción: ¿Por qué un Menu Builder en Laravel?

Si has desarrollado aplicaciones con Laravel, sabes que gestionar menús estáticos en Blade se convierte en una pesadilla cuando tu app crece.

Un gestor de menús dinámicos centraliza toda la lógica de navegación, la hace reutilizable sin ensuciar tus vistas.

En este tutorial, crearás un sistema listo para producción con Laravel 12, usando:

  • Gates para control de acceso
  • Clases Builder para modularidad
  • Caching para performance
  • HTML semántico para accesibilidad

¿Quieres el código completo? Descarga el código al final del artículo.

Configuración Inicial: Menumanager

Configuración Inicial: Menumanager

Configuración Inicial: Menumanager

Requisitos Previos {#requisitos}

  • Laravel 12+ instalado
  • PHP 8.3 o superior
  • Laravel/ui
  • Conocimientos básicos de Service Providers

Migración y Modelo: Menús

Migración y Modelo: Menús

Migración y Modelo: Menús

Paso 1: Crear la Migración

En la terminal (raíz del proyecto), ejecuta:

 

php artisan make:migration create_menus_table

Esto genera un archivo en database/migrations/ (ej. 2023_xx_xx_create_menus_table.php). Ábrelo y reemplaza el contenido con esto:

PHP



use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('menus', function (Blueprint $table) {
            $table->id();
            $table->string('label'); // Texto visible
            $table->string('url')->nullable(); // Para enlaces manuales
            $table->string('target')->default('_self'); // _self o _blank
            $table->unsignedBigInteger('parent_id')->nullable(); // Para submenús
            $table->integer('order')->default(0); // Orden visual
            
            // Polimorfismo (menuable_id, menuable_type)
            // Permite vincular: Pages, Posts, Services
            $table->nullableMorphs('menuable'); 
            
            $table->timestamps();

            // FK recursiva
            $table->foreign('parent_id')->references('id')->on('menus')->onDelete('cascade');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('menus');
    }
};

Paso 2: Ejecutar la Migración

php artisan migrate

¡Listo! La tabla menus está creada. Si quieres rollback: php artisan migrate:rollback.

Paso 3: Crear el Modelo

Ejecuta:

php artisan make:model Menu

Esto genera app/Models/Menu.php. Ábrelo y reemplaza con esto:

PHP



namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class Menu extends Model
{
    protected $fillable = ['label', 'url', 'target', 'parent_id', 'order', 'menuable_id', 'menuable_type'];

    // Relación con el objeto real (Page)
    public function menuable(): MorphTo
    {
        return $this->morphTo();
    }

    // Relación recursiva: Hijos
    public function children()
    {
        return $this->hasMany(Menu::class, 'parent_id')->orderBy('order');
    }

    // Accesor: Obtener la URL final automáticamente    
    public function getHrefAttribute()
    {
        // 1. Si es un enlace personalizado manual, retornar eso.
        if ($this->url) {
            return $this->url;
        }

        // 2. Si está vinculado a un modelo (DB)
        if ($this->menuable) {
            // Obtenemos el slug del registro vinculado
            $slug = $this->menuable->slug; 

            // Usamos match (PHP 8+) para decidir el prefijo de la URL
            return match ($this->menuable_type) {
                'App\Models\Page'    => url("/page/{$slug}"),
                default              => '#'
            };
        }

        return '#';
    }
}

¡Ya está! El modelo soporta submenús recursivos, enlaces manuales y URLs dinámicas vía polimorfismo. Prueba creando un menú: php artisan tinker y Menu::create(['label' => 'Prueba', 'url' => '/']);.


Migración y Modelo: Pages

Migración y Modelo: Pages

Migración y Modelo: Pages

Migración

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('pages', function (Blueprint $table) {
            $table->id();
            $table->string('slug',100);
            $table->string('name',100)->unique();
            $table->text('content')->nullable();
            $table->string('image_url',100)->nullable();
            $table->string('meta_title',67);
            $table->string('meta_description',170);
            $table->integer('pos')->default(0);
            $table->boolean('active')->default(0);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('pages');
    }
};

Modelo:

namespace App\Models;

use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;

class Page extends Model
{
    public $timestamps = false;
    protected $fillable = [
        'slug',
        'name',        
        'content',
        'image_url',
        'meta_title',
        'meta_description',
        'pos',
        'active'
    ];
    public static function boot()
    {
        parent::boot();
        static::saving(function ($page) {
            $page->slug = Str::slug($page->name);
        });
    }
}

 


Seeders : Menus, Pages

Seeders : Menus, Pages

Seeders : Menus, Pages

Aquí tienes el flujo completo, desde la creación hasta la ejecución, con buenas prácticas integradas:

Comando de Creación

Primero genera el archivo (si aún no lo tienes):

php artisan make:seeder MenuSeeder

Eso creará database/seeders/MenuSeeder.php con la estructura básica.


Seeder Completo y Optimizado

Reemplaza el contenido con esta versión escalable:

namespace Database\Seeders;

use App\Models\Menu;
use Illuminate\Database\Seeder;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;

class MenuSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // Inicio (Enlace Estático Personalizado)
        Menu::create([
            'label' => 'Inicio',
            'url'   => '/',
            'order' => 0,
            'target'=> '_self'
        ]);

        // Blog (Enlace Estático Personalizado)
        Menu::create([
            'label' => 'Blog',
            'url'   => '/blog',
            'order' => 1,
        ]);

        Menu::create([
            'label'  => 'Link Externo',
            'url'    => 'https://whatsapp.com',
            'target' => '_blank', // Abrir en nueva pestaña
            'order'  => 4,
        ]);
    }
}

Instrucciones de Ejecución

Opción A - Ejecutar solo este seeder:

php artisan db:seed --class=MenuSeeder

Opción B - Incluirlo en el DatabaseSeeder principal (database/seeders/DatabaseSeeder.php):

public function run(): void { 
$this->call([ MenuSeeder::class, // otros seeders... ]); 
}

Luego ejecuta:

php artisan db:seed

Opción C - En desarrollo, migrar y sembrar todo de nuevo:

php artisan migrate:fresh --seed

Claves de Mejora

  • Estructura array: Fácil de leer y expandir

  • firstOrCreate(): Idempotente (ejecuta múltiples veces sin duplicar)

  • use WithoutModelEvents: Mejora el rendimiento

  • ✅ Verifica tu migración tenga los campos necesarios

Page:

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Page; // Importa el modelo Page
use Illuminate\Support\Str; // Importa la clase Str para generar el slug

class PageSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
Page::create([
            'name' => 'Acerca de Nosotros',
            'slug'=>'nosotros',
            'content' => 'Contenido detallado sobre la misión y visión de la empresa.',
            'image_url' => 'images/about-us.jpg',
            'meta_title' => 'Conoce Nuestra Empresa',
            'meta_description' => 'Información clave sobre quiénes somos.',
            'pos' => 1,
            'active' => true,
        ]);

        // Segundo Registro
        Page::create([
            'name' => 'Política de Privacidad',
            'slug'=>'politicas',
            'content' => 'Documento completo sobre nuestra política de manejo de datos.',
            'image_url' => 'images/privacy.jpg',
            'meta_title' => 'Política de Datos',
            'meta_description' => 'Revisa cómo protegemos tu información.',
            'pos' => 2,
            'active' => true,
        ]);

        // Tercer Registro
        Page::create([
            'name' => 'Términos y Condiciones',
            'slug'=>'terminos',
            'content' => 'Reglas y acuerdos para el uso de nuestro servicio.',
            'image_url' => 'images/terms.jpg',
            'meta_title' => 'Términos de Uso',
            'meta_description' => 'Consulta nuestros términos legales.',
            'pos' => 3,
            'active' => true,
        ]);
    }
}

🏃 Pasos para Ejecutar el Seeder

Para que estos datos se inserten en tu base de datos, debes seguir dos pasos:

Registrar el Seeder

 

Abre el archivo database/seeders/DatabaseSeeder.php y llama a tu nuevo seeder dentro del método run:

PHP

// database/seeders/DatabaseSeeder.php

use Database\Seeders\PageSeeder; // Importa tu seeder

public function run()
{
    // Llama a tu seeder
    $this->call(PageSeeder::class);
    // ... otros seeders si los tienes
}

 

Ejecutar la Siembra

 

Finalmente, ejecuta el comando Artisan para poblar la base de datos:

 

php artisan db:seed

O, si quieres refrescar las migraciones y sembrar de nuevo (¡CUIDADO, esto borrará todos los datos existentes en tus tablas!):

 

php artisan migrate:fresh --seed

Esto insertará los tres registros con los datos que definiste en la tabla pages (asumiendo que tu tabla se llama así por convención de Laravel).


Rutas

Rutas

Rutas


// Rutas del Gestor de Menú

Route::get('/menu', [MenuController::class, 'index'])->name('admin.menu.index');

Route::post('/menu/store', [MenuController::class, 'store'])->name('admin.menu.store');

Route::post('/menu/update-tree', [MenuController::class, 'updateTree'])->name('admin.menu.updateTree'); // AJAX Orden

Route::put('/menu/{menuItem}', [MenuController::class, 'update'])->name('admin.menu.update'); // AJAX Edición rápida

Route::delete('/menu/{menuItem}', [MenuController::class, 'destroy'])->name('admin.menu.destroy');

 

Estas rutas se definen en routes/web.php (o en un archivo de rutas admin con middleware de autenticación). Forman un CRUD personalizado para menús, con énfasis en operaciones AJAX para un gestor de árbol (orden y edición sin recarga). Cada ruta incluye método HTTP, endpoint, controlador/método, nombre y propósito.

  1. GET /menu
    • Controlador: MenuController@index
    • Nombre: menu.index
    • Propósito: Muestra la vista principal del gestor, cargando todos los menús como árbol (raíces con hijos recursivos) para visualización y edición.
  2. POST /menu/store
    • Controlador: MenuController@store
    • Nombre: menu.store
    • Propósito: Crea un nuevo ítem de menú. Recibe datos del formulario o AJAX (label, url, parent_id, order, etc.). Calcula el orden automáticamente si no se proporciona. Devuelve JSON con el nuevo menú.
  3. POST /menu/update-tree
    • Controlador: MenuController@updateTree
    • Nombre: menu.updateTree
    • Propósito: Actualiza la estructura y orden del árbol completo vía AJAX. Espera un array JSON de objetos {id, parent_id, order} desde el frontend (ej. después de drag-and-drop). Realiza un bulk update eficiente en la base de datos.
  4. PUT /menu/{menuItem}
    • Controlador: MenuController@update (inyección de modelo por ID en {menuItem})
    • Nombre: menu.update
    • Propósito: Actualiza un ítem específico vía AJAX (edición inline). Recibe campos como label, url, target o menuable. Devuelve JSON con el menú actualizado. No afecta el orden del árbol.
  5. DELETE /menu/{menuItem}
    • Controlador: MenuController@destroy (inyección de modelo por ID en {menuItem})
    • Nombre: menu.destroy
    • Propósito: Elimina un ítem de menú (y sus hijos por cascade de la FK). Verifica si tiene submenús antes de borrar. Devuelve JSON con estado de éxito o error (ej. si no se puede eliminar por dependencias).

Notas:

  • Las rutas 2-5 son AJAX-friendly y devuelven JSON para integración con frontend (JS/Vue).
  • Usa route('menu.index') en vistas para generar URLs.
  • Recomendación: Agrupa con Route::prefix('admin')->middleware(['auth', 'admin']) para seguridad.
  • Para listar: php artisan route:list --name=menu.

El Controlador: MenuController

El Controlador: MenuController

El Controlador: MenuController

Controlador MenuController

 

use App\Models\Menu;
use App\Models\Page;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class MenuController extends Controller
{
    public function index()
    {
        // Cargar solo raíces, eager loading de hijos
        $menuItems = Menu::whereNull('parent_id')->with('children')->orderBy('order')->get();
        
        // Datos para llenar los selectores laterales
        $pages = Page::select('id', 'name')->get();     
       

        return view('admin.menu.index', compact('menuItems', 'pages'));
    }

    public function store(Request $request)
    {
        // Agregar enlace personalizado
        if ($request->type === 'custom') {
            Menu::create([
                'label' => $request->label,
                'url'   => $request->url,
                'target' => '_self'
            ]);
            return back()->with('success', 'Enlace agregado.');
        }

        // Agregar Items desde BD (Páginas/Servicios)
        if ($request->has('items')) {
            $class = "App\\Models\\" . $request->type;
            foreach ($request->items as $id) {
                $model = $class::find($id);
                if ($model) {
                    Menu::create([
                        'label' => $model->title ?? $model->name, // Ajustar al campo de tu tabla
                        'menuable_type' => $class,
                        'menuable_id' => $model->id,
                    ]);
                }
            }
            return back()->with('success', 'Elementos agregados.');
        }
        return back();
    }

    // Guardar estructura (Drag & Drop)
    public function updateTree(Request $request)
    {
        $structure = $request->input('structure');
        $this->saveStructure($structure);
        return response()->json(['status' => 'ok']);
    }

    // Función recursiva para guardar orden y padres
    private function saveStructure($items, $parentId = null)
    {
        foreach ($items as $index => $item) {
            $menuItem = Menu::find($item['id']);
            if($menuItem) {
                $menuItem->parent_id = $parentId;
                $menuItem->order = $index;
                $menuItem->save();

                if (isset($item['children']) && !empty($item['children'])) {
                    $this->saveStructure($item['children'], $menuItem->id);
                }
            }
        }
    }

    // Actualizar Label o Target (Edición Rápida)
    public function update(Request $request, Menu $menuItem)
    {
        $menuItem->update($request->only(['label', 'target', 'url']));
        return back()->with('success', 'Ítem actualizado.');
    }

    public function destroy(Menu $menuItem)
    {
        $menuItem->delete(); // Borra en cascada por la migración
        return back()->with('success', 'Ítem eliminado.');
    }

}

 


Vistas blade para gestionar el menu

Vistas blade para gestionar el menu

Vistas blade para gestionar el menu

Ahora vamos por las vistas 

admin/menu/index.blade.php

@extends('layouts.app')
@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-10">
                <div class="card bg-white">
                    <div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
                        <b>Menu</b>
                        <div class="btn-group">
                            <button id="saveBtn" class="btn btn-light btn-sm"> Guardar Orden</button>
                        </div>
                    </div>
                    <div class="card-body">
                        @include('admin.partials.message')
                        <div class="row">
                            <div class="col-md-4">
                                <div class="accordion" id="accordionAdd">



                                    <!-- PAGINAS-->
                                    <div class="accordion-item">
                                        <h2 class="accordion-header">
                                            <button class="accordion-button collapsed" type="button"
                                                data-bs-toggle="collapse" data-bs-target="#cPages">Páginas</button>
                                        </h2>
                                        <div id="cPages" class="accordion-collapse collapse "
                                            data-bs-parent="#accordionAdd">
                                            <div class="accordion-body">
                                                <form action="{{ route('admin.menu.store') }}" method="POST">
                                                    @csrf
                                                    <input type="hidden" name="type" value="Page">
                                                    <div class="mb-3 overflow-auto" style="max-height:150px;">
                                                        @foreach ($pages as $p)
                                                            <div class="form-check">
                                                                <input class="form-check-input" type="checkbox"
                                                                    name="items[]" value="{{ $p->id }}">
                                                                <label class="form-check-label">{{ $p->name }}</label>
                                                            </div>
                                                        @endforeach
                                                    </div>
                                                    <button class="btn btn-sm btn-outline-secondary w-100">Agregar</button>
                                                </form>
                                            </div>
                                        </div>
                                    </div>
                                    <!-- PERSONALIZADO-->
                                    <div class="accordion-item">
                                        <h2 class="accordion-header"><button class="accordion-button collapsed"
                                                type="button" data-bs-toggle="collapse" data-bs-target="#cCustom">Enlace
                                                Personalizado</button></h2>
                                        <div id="cCustom" class="accordion-collapse collapse"
                                            data-bs-parent="#accordionAdd">
                                            <div class="accordion-body">
                                                <form action="{{ route('admin.menu.store') }}" method="POST">
                                                    @csrf
                                                    <input type="hidden" name="type" value="custom">
                                                    <input type="text" name="label"
                                                        class="form-control form-control-sm mb-2" placeholder="Etiqueta"
                                                        required>
                                                    <input type="text" name="url"
                                                        class="form-control form-control-sm mb-2" placeholder="http://"
                                                        required>
                                                    <button class="btn btn-sm btn-outline-secondary w-100">Agregar</button>
                                                </form>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </div>

                            <div class="col-md-8">
                                <ul id="menu-root" class="list-group sortable-list">
                                    @foreach ($menuItems as $item)
                                        @include('admin.partials.item', ['item' => $item])
                                    @endforeach
                                </ul>
                            </div>
                        </div>
                        <div class="modal fade" id="editModal" tabindex="-1">
                            <div class="modal-dialog">
                                <form id="editForm" method="POST" class="modal-content">
                                    @csrf
                                    @method('PUT')
                                    <div class="modal-header">
                                        <h5 class="modal-title">Editar Ítem</h5>
                                        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                                    </div>
                                    <div class="modal-body">
                                        <div class="mb-3">
                                            <label>Etiqueta</label>
                                            <input type="text" name="label" id="modalLabel" class="form-control">
                                        </div>
                                        <div class="mb-3">
                                            <label>URL (Solo si es personalizado)</label>
                                            <input type="text" name="url" id="modalUrl" class="form-control">
                                        </div>
                                        <div class="mb-3">
                                            <label>Abrir en</label>
                                            <select name="target" id="modalTarget" class="form-select">
                                                <option value="_self">Misma pestaña</option>
                                                <option value="_blank">Nueva pestaña</option>
                                            </select>
                                        </div>
                                    </div>
                                    <div class="modal-footer">
                                        <button type="submit" class="btn btn-primary">Guardar Cambios</button>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
        // 1. Configurar Sortable (Drag & Drop) Recursivo
        function initSortable() {
            var nestedSortables = [].slice.call(document.querySelectorAll('.sortable-list'));
            nestedSortables.forEach(function(el) {
                new Sortable(el, {
                    group: 'nested',
                    animation: 150,
                    fallbackOnBody: true,
                    swapThreshold: 0.65
                });
            });
        }
        initSortable();

        // 2. Guardar Orden
        document.getElementById('saveBtn').addEventListener('click', function() {
            let structure = serializeMenu(document.getElementById('menu-root'));
            axios.post('{{ route('admin.menu.updateTree') }}', {
                structure: structure,
                _token: '{{ csrf_token() }}'
            }).then(res => alert('Orden guardado!')).catch(err => alert('Error'));
        });

        function serializeMenu(list) {
            let items = [];
            [...list.children].forEach(li => {
                if (li.dataset.id) {
                    let item = {
                        id: li.dataset.id,
                        children: []
                    };
                    let subList = li.querySelector('ul');
                    if (subList) item.children = serializeMenu(subList);
                    items.push(item);
                }
            });
            return items;
        }

        // 3. Lógica del Modal Editar
        var editModal = document.getElementById('editModal');
        editModal.addEventListener('show.bs.modal', function(event) {
            var button = event.relatedTarget;
            var id = button.getAttribute('data-id');
            var label = button.getAttribute('data-label');
            var url = button.getAttribute('data-url');
            var target = button.getAttribute('data-target');

            var form = document.getElementById('editForm');
            form.action = '/admin/menu/' + id; // URL update

            document.getElementById('modalLabel').value = label;
            document.getElementById('modalUrl').value = url;
            document.getElementById('modalTarget').value = target;
        });
    </script>
    <style>
        .sortable-list {
            min-height: 20px;
        }

        .menu-item-card {
            cursor: move;
            background: #fff;
        }

        .menu-item-card:hover {
            background: #f8f9fa;
        }

        /* Indentación visual para subniveles */
        .nested-list {
            margin-left: 25px;
            border-left: 2px dashed #ddd;
            padding-left: 10px;
            margin-top: 5px;
        }

        .sortable-ghost {
            opacity: 0.4;
            background: #e2e6ea;
            border: 1px dashed #999;
        }
    </style>
@endsection


 

admin/partials/item.blade.php

 

<li class="list-group-item menu-item-card border mb-2 rounded" data-id="{{ $item->id }}">
    <div class="d-flex justify-content-between align-items-center py-1">
        <div>
            <i class="bi bi-grip-vertical text-muted me-2"></i>
            <span class="fw-bold">{{ $item->label }}</span>
            <small class="text-muted ms-2 fst-italic">
                ({{ $item->menuable_type ? class_basename($item->menuable_type) : 'Link' }})
            </small>
        </div>
        <div>
            <button class="btn btn-sm btn-link text-decoration-none"
                    data-bs-toggle="modal" data-bs-target="#editModal"
                    data-id="{{ $item->id }}"
                    data-label="{{ $item->label }}"
                    data-url="{{ $item->url }}"
                    data-target="{{ $item->target }}">
                <i class="bi bi-pencil"></i>
            </button>

            <form action="{{ route('admin.menu.destroy', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('¿Borrar?');">
                @csrf @method('DELETE')
                <button class="btn btn-sm btn-link text-danger"><i class="bi bi-trash"></i></button>
            </form>
        </div>
    </div>

    <ul class="list-group sortable-list nested-list">
        @foreach($item->children as $child)
            @include('admin.menu.partial_item', ['item' => $child])
        @endforeach
    </ul>
</li>

 

 

admin/partials/message.blade.php

 

@if (session('successMessage'))
    <div class="alert alert-success">
        {{ session('successMessage') }}
    </div>
@endif

@if (session('errorMessage'))
    <div class="alert alert-danger">
        {{ session('errorMessage') }}
    </div>
@endif
@if ($errors->any())
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

ViewComposer: MenuComposerServiceProvider

ViewComposer: MenuComposerServiceProvider

ViewComposer: MenuComposerServiceProvider

Si ya generaste el provider con php artisan make:provider ViewComposerServiceProvider, Artisan lo agrega automáticamente a bootstrap/providers.php. Si no, hazlo manual.

Paso 1: Generar (si no lo hiciste)

Bash

php artisan make:provider MenuComposerServiceProvider
  • Esto crea el archivo y lo registra solo en bootstrap/providers.php.

Paso 2: Editar el código

Abre app/Providers/ViewComposerServiceProvider.php y pega el código del ViewComposer (el que te di antes).

Paso 3: Verificar registro

Abre bootstrap/providers.php (créalo si no existe). Debe verse así:

PHP

return [
    App\Providers\AppServiceProvider::class,
    App\Providers\AuthServiceProvider::class,
    // ... otros que vengan por defecto
    App\Providers\MenuComposerServiceProvider::class,  // ¡Este se agrega auto!
];

 

Implementación 

namespace App\Providers;

use App\Models\Menu;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;

class MenuComposerServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap services.
     */
    public function boot(): void
    {        

        View::composer('*', function ($view) {
            // Carga solo menús padres con sus hijos directos
            $view->with([
                '_menu' => Menu::whereNull('parent_id')->with('children')->orderBy('order')->get()
            ]);
        });

    }
}

 


El Menú de navegación dinámico

Finalmente implementamos nuestro menu de navegación en el layout

 

<ul class="navbar-nav mx-auto">
                        @foreach ($_menu as $item)
                            @if ($item->children->isEmpty())
                                {{-- CASO 1: ENLACE SIMPLE (Sin hijos) --}}
                                <li class="nav-item">
                                    <a class="nav-link {{ request()->url() == $item->href ? 'active fw-bold' : '' }}"
                                        href="{{ $item->href }}" target="{{ $item->target }}">
                                        {{ $item->label }}
                                    </a>
                                </li>
                            @else
                                {{-- CASO 2: DROPDOWN (Con hijos) --}}
                                <li class="nav-item dropdown">
                                    <a class="nav-link dropdown-toggle" href="#" role="button"
                                        data-bs-toggle="dropdown">
                                        {{ $item->label }}
                                    </a>
                                    <ul class="dropdown-menu">
                                        @foreach ($item->children as $child)
                                            <li>
                                                <a class="dropdown-item {{ request()->url() == $child->href ? 'active' : '' }}"
                                                    href="{{ $child->href }}" target="{{ $child->target }}">
                                                    {{ $child->label }}
                                                </a>
                                            </li>
                                        @endforeach
                                    </ul>
                                </li>
                            @endif
                        @endforeach
                    </ul>

Archivos que incluyen en la descarga

Archivos que incluyen en la descarga

Archivos que incluyen en la descarga

Los archivos del Menumanager en Laravel que se incluyen en la descarga son los esenciales para integrarlo en un proyecto Laravel

  • app
  • database
  • resources
  • routes


Leido 233 veces | 2 usuarios

Descarga del código fuente

MenuManager en Laravel

Accede al código fuente esencial de nuestra aplicación en formato ZIP ó TXT. Ideal para desarrolladores que desean personalizar o integrar nuestra solución.

  • [ Pago: Paypal ]
  • [ Precio: USD 7.00 ]
  • [ Descargas: 3 ]

Compartir link del tutorial con tus amigos

CÓDIGO FUENTE: USD 7.00

Conversar con J.Luis

Codea Applications

México, Colombia, España, Venezuela, Argentina, Bolivia, Perú