¿Qué vamos a construir?
Un sistema de roles y permisos completo con panel de administración. Al terminar tendrás:
- Roles dinámicos gestionados desde el panel (Super Admin, Editor, etc.)
- Permisos granulares por recurso:
viewAny,create,update,deletey más - Usuarios con rol asignable desde el formulario de edición en MoonShine
- El rol Super Admin con acceso total sin restricciones
Tres paquetes trabajando juntos:
spatie/laravel-permission → la lógica de roles y permisos
moonshine/moonshine → el panel de administración
sweet1s/moonshine-roles-permissions → el puente entre los dos
Instalar los paquetes
Partimos de un proyecto Laravel 12 limpio. Instalamos los tres paquetes en orden:
# 1. MoonShine 4
composer require moonshine/moonshine
php artisan moonshine:install
# 2. Spatie Laravel Permission
composer require spatie/laravel-permission
# 3. Puente MoonShine ↔ Spatie
composer require sweet1s/moonshine-roles-permissions
El comando moonshine:install publica la configuración, crea el MoonShineServiceProvider y registra las rutas del panel en /moonshine.
.env antes de continuar. El siguiente paso ejecuta migraciones.
Publicar migraciones y migrar
Spatie necesita publicar sus migraciones antes de ejecutarlas:
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
Esto crea cinco tablas nuevas: roles, permissions, model_has_roles, model_has_permissions y role_has_permissions.
Configurar el modelo User
El modelo User necesita el trait del paquete sweet1s en lugar del trait estándar de Spatie. Abre app/Models/User.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Sweet1s\MoonshineRBAC\Traits\MoonshineRBACHasRoles;
class User extends Authenticatable
{
use HasFactory, Notifiable, MoonshineRBACHasRoles;
const SUPER_ADMIN_ROLE_ID = 1;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
MoonshineRBACHasRoles extiende internamente el trait HasRoles de Spatie, así que no necesitás agregar los dos por separado. La constante SUPER_ADMIN_ROLE_ID = 1 le indica al sistema cuál rol tiene acceso total.
Crear el modelo Role
Necesitamos un modelo Role propio que extienda el de Spatie y agregue el comportamiento de MoonShine:
php artisan make:model Role
Reemplazá el contenido generado por este:
<?php
namespace App\Models;
use Spatie\Permission\Models\Role as SpatieRole;
use Sweet1s\MoonshineRBAC\Traits\HasMoonShineRolePermissions;
class Role extends SpatieRole
{
use HasMoonShineRolePermissions;
protected $with = ['permissions'];
}
El $with = ['permissions'] carga eager las permissions del rol en cada consulta, evitando el problema N+1 cuando MoonShine muestra la lista de roles.
Instalar el sistema RBAC
Este comando del paquete sweet1s hace tres cosas: genera los permisos automáticos para cada recurso registrado, crea el rol Super Admin (ID 1) y le asigna todos los permisos:
php artisan moonshine-rbac:install
Los permisos que genera para cada recurso son:
viewAny → ver el listado
view → ver detalle
create → crear registros
update → editar registros
delete → eliminar
massDelete → eliminación masiva
restore → restaurar eliminados
forceDelete → eliminación permanente
Crear los Resources en MoonShine
Necesitamos tres resources: uno para roles, uno para permisos y uno para usuarios.
RoleResource — gestión de roles con asignación de permisos:
<?php
declare(strict_types=1);
namespace App\MoonShine\Resources;
use App\Models\Role;
use MoonShine\Laravel\Resources\ModelResource;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Number;
use MoonShine\UI\Fields\Text;
use Sweet1s\MoonshineRBAC\Traits\WithPermissionsFormComponent;
use Sweet1s\MoonshineRBAC\Traits\WithRolePermissions;
class RoleResource extends ModelResource
{
use WithRolePermissions;
use WithPermissionsFormComponent;
protected string $model = Role::class;
protected string $column = 'name';
public function getTitle(): string
{
return 'Roles';
}
protected function indexFields(): iterable
{
return [
ID::make(),
Text::make('Name', 'name'),
Number::make('Priority', 'priority'),
];
}
protected function formFields(): iterable
{
return [
ID::make(),
Text::make('Name', 'name')->required(),
Number::make('Priority', 'priority')->default(0),
];
}
public function rules(mixed $item): array
{
return [
'name' => 'required|string|max:255',
'priority' => 'nullable|integer',
];
}
public function search(): array
{
return ['id', 'name'];
}
}
El trait WithPermissionsFormComponent inyecta automáticamente un componente de checkboxes en el formulario para asignar permisos al rol.
PermissionResource — CRUD de permisos individuales:
<?php
declare(strict_types=1);
namespace App\MoonShine\Resources;
use MoonShine\Laravel\Resources\ModelResource;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Text;
use Spatie\Permission\Models\Permission;
use Sweet1s\MoonshineRBAC\Traits\WithRolePermissions;
class PermissionResource extends ModelResource
{
use WithRolePermissions;
protected string $model = Permission::class;
protected string $column = 'name';
public function getTitle(): string
{
return 'Permissions';
}
protected function indexFields(): iterable
{
return [
ID::make(),
Text::make('Name', 'name'),
Text::make('Guard', 'guard_name'),
];
}
protected function formFields(): iterable
{
return [
ID::make(),
Text::make('Name', 'name')->required(),
Text::make('Guard', 'guard_name')->default('web'),
];
}
public function rules(mixed $item): array
{
return [
'name' => 'required|string|max:255',
'guard_name' => 'required|string|max:255',
];
}
}
UserResource — usuarios con selector de rol en el formulario:
<?php
declare(strict_types=1);
namespace App\MoonShine\Resources;
use App\Models\User;
use Illuminate\Validation\Rule;
use MoonShine\Laravel\Resources\ModelResource;
use MoonShine\UI\Components\Layout\Box;
use MoonShine\UI\Components\Layout\Column;
use MoonShine\UI\Components\Layout\Grid;
use MoonShine\UI\Fields\Date;
use MoonShine\UI\Fields\Email;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Password;
use MoonShine\UI\Fields\PasswordRepeat;
use MoonShine\UI\Fields\Text;
use Sweet1s\MoonshineRBAC\Traits\WithRoleFormComponent;
use Sweet1s\MoonshineRBAC\Traits\WithRolePermissions;
class UserResource extends ModelResource
{
use WithRolePermissions;
use WithRoleFormComponent;
protected string $model = User::class;
protected string $column = 'name';
public function getTitle(): string
{
return 'Users';
}
protected function indexFields(): iterable
{
return [
ID::make(),
Text::make('Name', 'name'),
Email::make('Email', 'email'),
Date::make('Created at', 'created_at')->format('d.m.Y'),
];
}
protected function formFields(): iterable
{
return [
Grid::make([
Column::make([
Box::make('Información principal', [
ID::make(),
Text::make('Name', 'name')->required(),
Email::make('Email', 'email')->required(),
]),
Box::make('Cambiar contraseña', [
Password::make('Password', 'password')
->customAttributes(['autocomplete' => 'new-password'])
->eye(),
PasswordRepeat::make('Repeat Password', 'password_repeat')
->customAttributes(['autocomplete' => 'confirm-password'])
->eye(),
]),
]),
]),
];
}
public function rules(mixed $item): array
{
return [
'name' => 'required',
'email' => [
'sometimes', 'bail', 'required', 'email',
Rule::unique('users')->ignoreModel($item),
],
'password' => $item->exists
? 'sometimes|nullable|min:6|same:password_repeat'
: 'required|min:6|same:password_repeat',
];
}
public function search(): array
{
return ['id', 'name', 'email'];
}
}
El trait WithRoleFormComponent agrega el selector de rol al formulario de edición de usuario automáticamente.
Registrar los Resources
En app/Providers/MoonShineServiceProvider.php registramos los tres resources:
<?php
declare(strict_types=1);
namespace App\Providers;
use App\MoonShine\Resources\PermissionResource;
use App\MoonShine\Resources\RoleResource;
use App\MoonShine\Resources\UserResource;
use Illuminate\Support\ServiceProvider;
use MoonShine\Contracts\Core\DependencyInjection\CoreContract;
use MoonShine\Laravel\DependencyInjection\MoonShine;
use MoonShine\Laravel\DependencyInjection\MoonShineConfigurator;
class MoonShineServiceProvider extends ServiceProvider
{
public function boot(CoreContract $core): void
{
$core
->resources([
UserResource::class,
RoleResource::class,
PermissionResource::class,
])
->pages([
...$core->getConfig()->getPages(),
]);
}
}
Crear el usuario administrador
Podés crear el admin con un seeder o directamente desde Tinker.
Opción A — Seeder (recomendado para proyectos en equipo):
<?php
namespace Database\Seeders;
use App\Models\Role;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class AdminUserSeeder extends Seeder
{
public function run(): void
{
$admin = User::create([
'name' => 'Administrador',
'email' => 'admin@admin.com',
'password' => Hash::make('password'),
'email_verified_at' => now(),
]);
$superAdminRole = Role::find(1);
if ($superAdminRole) {
$admin->assignRole($superAdminRole);
$this->command->info('Admin creado: admin@admin.com / password');
} else {
$this->command->error('Primero ejecutá: php artisan moonshine-rbac:install');
}
}
}
php artisan db:seed --class=AdminUserSeeder
Opción B — Tinker (para pruebas rápidas):
php artisan tinker
$admin = App\Models\User::create([
'name' => 'Administrador',
'email' => 'admin@admin.com',
'password' => Hash::make('password'),
'email_verified_at' => now(),
]);
$admin->assignRole(App\Models\Role::find(1));
Ingresar al panel
Abrí tu navegador en:
http://tu-proyecto.test/moonshine
Ingresá con admin@admin.com / password. El panel mostrará tres secciones en el menú:
- Users — listado de usuarios con columnas name, email, fecha. El formulario de edición incluye el selector de rol.
- Roles — listado de roles. Al crear o editar un rol aparecen checkboxes con todos los permisos disponibles agrupados por resource.
- Permissions — listado de permisos individuales con su guard.
Problemas comunes
El menú de roles no aparece en el panel:
php artisan cache:clear
php artisan config:clear
php artisan moonshine-rbac:install
Error "Class Role not found" al asignar rol:
Verificá que en config/permission.php el modelo de roles apunta al tuyo:
'models' => [
'role' => App\Models\Role::class,
'permission' => Spatie\Permission\Models\Permission::class,
],
Los permisos de un Resource nuevo no aparecen:
Registrá el Resource en MoonShineServiceProvider y volvé a ejecutar:
php artisan moonshine-rbac:install