En este tutorial tendremos 2 usuarios (administrador y empleado). El administrador tendrá todas las acciones sobre una Tarea, en cambio el empleado solo podrá ver las tareas que se le asignó.
He preparado un pequeño proyecto base que contiene todo lo necesario para empezar el tutorial, lo puedes descargar aquí. Lo que contiene es:
Luego de clonar el repositorio, ingresamos a la carpeta y nos vamos al branch starter con el siguiente comando.
1git checkout starter
Luego creamos las tablas con información de prueba en la base de datos con el siguiente comando.
1php artisan migrate --seed
Las Policies es una característica que Laravel nos provee para que podamos asignar quién podrá ejecutar acciones sobre un modelo en particular.
Como puedes ver en la imagen, cada modelo tiene ciertas acciones y nosotros podemos autorizar quién podrá ejecutar cada acción.
Para crear el policy del modelo Task, ejecutamos el comando:
1php artisan make:policy TaskPolicy --model=Task
Lo que nos creará el archivo app/Policies/TaskPolicy.php
1<?php 2 3namespace App\Policies; 4 5use App\Models\Task; 6use App\Models\User; 7use Illuminate\Auth\Access\Response; 8 9class TaskPolicy10{11 /**12 * Determine whether the user can view any models.13 */14 public function viewAny(User $user): bool15 {16 //17 }18 19 /**20 * Determine whether the user can view the model.21 */22 public function view(User $user, Task $task): bool23 {24 //25 }26 27 /**28 * Determine whether the user can create models.29 */30 public function create(User $user): bool31 {32 //33 }34 35 /**36 * Determine whether the user can update the model.37 */38 public function update(User $user, Task $task): bool39 {40 //41 }42 43 /**44 * Determine whether the user can delete the model.45 */46 public function delete(User $user, Task $task): bool47 {48 //49 }50 51 /**52 * Determine whether the user can restore the model.53 */54 public function restore(User $user, Task $task): bool55 {56 //57 }58 59 /**60 * Determine whether the user can permanently delete the model.61 */62 public function forceDelete(User $user, Task $task): bool63 {64 //65 }66}
Cada función del policy está relacionado a cada acción del controlador tal como se ve en la documentación oficial.
Para hacer esa relación entre las funciones del Policy y el Controlador, tenemos que autorizar en el constructor del controlador que usaremos el Policy del Task.
Además modificaremos la lógica de la función index que nos retorna todas las tareas para que en caso el usuario sea empleado nos traiga solo las que se les ha asignado.
1<?php 2 3namespace App\Http\Controllers; 4 5use App\Models\Task; 6use App\Models\User; 7use Illuminate\Http\Request; 8 9class TaskController extends Controller10{11 /** // 12 * Create the controller instance.13 */14 public function __construct()15 {16 $this->authorizeResource(Task::class, 'task');17 }18 19 public function index()20 {21 $tasks = Task::query()22 ->with('user')23 ->when(auth()->user()->isEmployee(), function ($query) { 24 return $query->whereUserId(auth()->id());25 })26 ->get();27 28 return view('task-index', compact('tasks'));29 }30 31 public function create()32 {33 $employees = User::select(['id', 'name'])34 ->whereRole(User::ROLE_EMPLOYEE)35 ->get();36 37 return view('task-create', compact('employees'));38 }39 40 public function store(Request $request)41 {42 $validated = $request->validate([43 'title' => 'required|string|max:100',44 'description' => 'required|string|max:300',45 'user_id' => 'required|exists:users,id'46 ]);47 48 Task::create($validated);49 50 return redirect(route('tasks.index'));51 }52 53 public function show(Task $task)54 {55 $task->load('user');56 57 return view('task-show', compact('task'));58 }59 60 public function edit(Task $task)61 {62 }63 64 public function update(Request $request, Task $task)65 {66 }67 68 public function destroy(Task $task)69 {70 }71}
Como lo especificamos al empezar el blog, vamos hacer que solo el administrador pueda tener acceso a todas las acciones, para ello aplicamos la función "before" que se ejecutará antes que todos las funciones.
1<?php 2 3namespace App\Policies; 4 5use App\Models\Task; 6use App\Models\User; 7use Illuminate\Auth\Access\Response; 8 9class TaskPolicy10{11 /** // 12 * Perform pre-authorization checks.13 */14 public function before(User $user, string $ability): bool|null15 {16 if ($user->isAdministrator()) {17 return true;18 }19 20 return null;21 }22 23 /**24 * Determine whether the user can view any models.25 */26 public function viewAny(User $user): bool27 {28 //29 }30 31 /**32 * Determine whether the user can view the model.33 */34 public function view(User $user, Task $task): bool35 {36 //37 }38 39 /**40 * Determine whether the user can create models.41 */42 public function create(User $user): bool43 {44 //45 }46 47 /**48 * Determine whether the user can update the model.49 */50 public function update(User $user, Task $task): bool51 {52 //53 }54 55 /**56 * Determine whether the user can delete the model.57 */58 public function delete(User $user, Task $task): bool59 {60 //61 }62 63 /**64 * Determine whether the user can restore the model.65 */66 public function restore(User $user, Task $task): bool67 {68 //69 }70 71 /**72 * Determine whether the user can permanently delete the model.73 */74 public function forceDelete(User $user, Task $task): bool75 {76 //77 }78}
Luego editaremos las siguientes funciones:
1public function viewAny(User $user): bool2{3 return true; 4}
1public function view(User $user, Task $task): bool2{3 return $user->isEmployee() && $user->id == $task->user_id; 4}
1public function create(User $user): bool 2{ 3 return false; 4} 5 6/** 7* Determine whether the user can update the model. 8*/ 9public function update(User $user, Task $task): bool10{11 return false; 12}13 14/**15* Determine whether the user can delete the model.16*/17public function delete(User $user, Task $task): bool18{19 return false; 20}21 22/**23* Determine whether the user can restore the model.24*/25public function restore(User $user, Task $task): bool26{27 return false; 28}29 30/**31* Determine whether the user can permanently delete the model.32*/33public function forceDelete(User $user, Task $task): bool34{35 return false; 36}
Estos son los usuarios que usaremos en nuestra aplicación que ya están en nuestra base de datos.
Administrador
Usuario: administrador@laraveleando.com
Contraseña: password
Empleados
Usuario: empleado1@laraveleando.com
Contraseña: password
Usuario: empleado2@laraveleando.com
Contraseña: password
Como podemos ver todo está funcionando como lo esperado, pero podemos hacer algo más. Dado que solo el administrador puede "Registrar una Tarea", vamos a ocultar ese item del menú si es que el usuario es empleado.
Laravel nos provee de directivas Blade como @can/@endcan que podemos usar para validar las acciones de las Policy, en este caso del modelo Task.
Ocultamos los items del menú tanto en desktop como en mobile en el archivo resources/views/layouts/navigation.blade.php.
1<nav x-data="{ open: false }" class="bg-white border-b border-gray-100"> 2 <!-- Primary Navigation Menu --> 3 <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> 4 <div class="flex justify-between h-16"> 5 <div class="flex"> 6 <!-- Logo --> 7 <div class="shrink-0 flex items-center"> 8 <a href="{{ route('dashboard') }}"> 9 <x-application-logo class="block h-9 w-auto fill-current text-gray-800"/> 10 </a> 11 </div> 12 13 <!-- Navigation Links --> 14 <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex"> 15 <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> 16 {{ __('Dashboard') }} 17 </x-nav-link> 18 <x-nav-link :href="route('tasks.index')" :active="request()->routeIs('tasks.index')"> 19 {{ __('Ver Tareas') }} 20 </x-nav-link> 21 @can('create', App\Models\Task::class) 22 <x-nav-link :href="route('tasks.create')" :active="request()->routeIs('tasks.create')"> 23 {{ __('Registrar Tarea') }} 24 </x-nav-link> 25 @endcan 26 </div> 27 </div> 28 29 <!-- Settings Dropdown --> 30 <div class="hidden sm:flex sm:items-center sm:ml-6"> 31 <x-dropdown align="right" width="48"> 32 <x-slot name="trigger"> 33 <button 34 class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"> 35 <div>{{ Auth::user()->name }}</div> 36 37 <div class="ml-1"> 38 <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" 39 viewBox="0 0 20 20"> 40 <path fill-rule="evenodd" 41 d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" 42 clip-rule="evenodd"/> 43 </svg> 44 </div> 45 </button> 46 </x-slot> 47 48 <x-slot name="content"> 49 <x-dropdown-link :href="route('profile.edit')"> 50 {{ __('Profile') }} 51 </x-dropdown-link> 52 53 <!-- Authentication --> 54 <form method="POST" action="{{ route('logout') }}"> 55 @csrf 56 57 <x-dropdown-link :href="route('logout')" 58 onclick="event.preventDefault(); 59 this.closest('form').submit();"> 60 {{ __('Log Out') }} 61 </x-dropdown-link> 62 </form> 63 </x-slot> 64 </x-dropdown> 65 </div> 66 67 <!-- Hamburger --> 68 <div class="-mr-2 flex items-center sm:hidden"> 69 <button @click="open = ! open" 70 class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out"> 71 <svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24"> 72 <path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" 73 stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 74 d="M4 6h16M4 12h16M4 18h16"/> 75 <path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" 76 stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> 77 </svg> 78 </button> 79 </div> 80 </div> 81 </div> 82 83 <!-- Responsive Navigation Menu --> 84 <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden"> 85 <div class="pt-2 pb-3 space-y-1"> 86 <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> 87 {{ __('Dashboard') }} 88 </x-responsive-nav-link> 89 <x-responsive-nav-link :href="route('tasks.index')" :active="request()->routeIs('tasks.index')"> 90 {{ __('Ver Tareas') }} 91 </x-responsive-nav-link> 92 @can('create', App\Models\Task::class) 93 <x-responsive-nav-link :href="route('tasks.create')" :active="request()->routeIs('tasks.create')"> 94 {{ __('Registrar Tarea') }} 95 </x-responsive-nav-link> 96 @endcan 97 </div> 98 99 <!-- Responsive Settings Options -->100 <div class="pt-4 pb-1 border-t border-gray-200">101 <div class="px-4">102 <div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>103 <div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>104 </div>105 106 <div class="mt-3 space-y-1">107 <x-responsive-nav-link :href="route('profile.edit')">108 {{ __('Profile') }}109 </x-responsive-nav-link>110 111 <!-- Authentication -->112 <form method="POST" action="{{ route('logout') }}">113 @csrf114 115 <x-responsive-nav-link :href="route('logout')"116 onclick="event.preventDefault();117 this.closest('form').submit();">118 {{ __('Log Out') }}119 </x-responsive-nav-link>120 </form>121 </div>122 </div>123 </div>124</nav>
Entramos como empleado y vemos que ya no nos aparece el item del menú "Registrar Tarea".
Vamos a crear algunos tests para darle más seguridad a nuestra aplicación y de esta manera ahorremos tiempo en probar cuando actualicemos o añadamos nuevas funcionalidades. Vamos a usar Pest que es un framework de testing para PHP.
Dado que toda la lógica está en nuestro controlador, una manera es que creamos el test con la misma ruta y nombre, por ello crearemos el test en la ruta tests/Feature/Http/Controllers/TaskControllerTest.php.
Estos son los tests que crearemos:
1<?php 2 3use App\Models\Task; 4use App\Models\User; 5use function Pest\Laravel\actingAs; 6 7it('returns all the tasks when user is administrator', function () { 8 9 // Preparación10 $admin = User::factory()11 ->administrator()12 ->create();13 14 User::factory()15 ->employee()16 ->has(Task::factory()->count(3))17 ->create();18 19 User::factory()20 ->employee()21 ->has(Task::factory()->count(2))22 ->create();23 24 // Acción y Confirmar25 actingAs($admin)26 ->get(route('tasks.index'))27 // La petición sea satisfactoria28 ->assertOk()29 // Nos retorne la vista task-index30 ->assertViewIs('task-index')31 // La vista retornada tenga todos los tasks registrados32 ->assertViewHas('tasks', Task::all());33});
1it('returns only the tasks assigned to the employee user', function () { 2 // Preparación 3 $employee01 = User::factory() 4 ->employee() 5 ->has(Task::factory()->count(3)) 6 ->create(); 7 8 $employee02 = User::factory() 9 ->employee()10 ->has(Task::factory()->count(3))11 ->create();12 13 // Acción y Confirmar14 actingAs($employee01)15 ->get(route('tasks.index'))16 // La petición sea satisfactoria17 ->assertOk()18 // Nos retorne la vista task-index19 ->assertViewIs('task-index')20 // La vista retornada solo las 3 tareas registradas21 ->assertViewHas('tasks', $employee01->tasks);22});
1it('returns create task view when user is administrator', function () { 2 // Preparación 3 $admin = User::factory() 4 ->administrator() 5 ->create(); 6 7 // Acción y Confirmar 8 actingAs($admin) 9 ->get(route('tasks.create'))10 // La petición sea satisfactoria11 ->assertOk()12 // Nos retorne la vista task-create13 ->assertViewIs('task-create');14});
1it('returns unauthorized action when user is employee and try to see the create task view', function () { 2 // Preparación 3 $employee = User::factory() 4 ->employee() 5 ->create(); 6 7 // Acción y Confirmar 8 9 actingAs($employee)10 ->get(route('tasks.create'))11 // La petición está prohibida12 ->assertForbidden();13});
1use function Pest\Laravel\assertDatabaseCount; 2use function Pest\Laravel\assertDatabaseHas; 3 4it('can register a task when user is administrator', function () { 5 // Preparación 6 $admin = User::factory() 7 ->administrator() 8 ->create(); 9 10 $employee = User::factory()11 ->employee()12 ->create();13 14 $task = [15 'title' => fake()->title,16 'description' => fake()->text(200),17 'user_id' => $employee->id18 ];19 20 // Acción y Confirmar21 actingAs($admin)22 ->post(route('tasks.store'), $task)23 ->assertRedirect(route('tasks.index'));24 25 // La tabla "tasks" tiene 1 registro26 assertDatabaseCount('tasks', 1);27 // La tabla "tasks" tiene el registro de la variable task28 assertDatabaseHas('tasks', $task);29});
1it('cannot register a task when user is employee', function () { 2 // Preparación 3 $employee = User::factory() 4 ->employee() 5 ->create(); 6 7 $task = [ 8 'title' => fake()->title, 9 'description' => fake()->text(200),10 'user_id' => $employee->id11 ];12 13 // Acción y Confirmar14 actingAs($employee)15 ->post(route('tasks.store'), $task)16 // La petición está prohibida17 ->assertForbidden();18 19 // La tabla tasks no tiene registros20 assertDatabaseCount('tasks', 0);21});
1it('will show any task if the user is administrador', function () { 2 // Preparación 3 $admin = User::factory() 4 ->administrator() 5 ->create(); 6 7 $employee01 = User::factory() 8 ->employee() 9 ->has(Task::factory()->count(3))10 ->create();11 12 // Acción y Confirmar13 actingAs($admin)14 ->get(route('tasks.show', $employee01->tasks->first()->id))15 // La petición sea satisfactoria16 ->assertOk()17 // Nos retorne la vista task-show18 ->assertViewIs('task-show')19 // La vista tiene la información del task del empleado 120 ->assertViewHas('task', $employee01->tasks->first());21});
1it('will show only the task related to the user if is an employee', function () { 2 // Preparación 3 $employee = User::factory() 4 ->employee() 5 ->has(Task::factory()->count(3)) 6 ->create(); 7 8 // Acción y Confirmar 9 actingAs($employee)10 ->get(route('tasks.show', $employee->tasks->first()->id))11 // La petición sea satisfactoria12 ->assertOk()13 // Nos retorne la vista task-show14 ->assertViewIs('task-show')15 // La vista tiene la información del task del empleado logueado16 ->assertViewHas('task', $employee->tasks->first());17});
1it('will not show the task related to other employees', function () { 2 // Preparación 3 $employee01 = User::factory() 4 ->employee() 5 ->has(Task::factory()->count(3)) 6 ->create(); 7 8 $employee02 = User::factory() 9 ->employee()10 ->has(Task::factory()->count(3))11 ->create();12 13 // Acción y Confirmar14 actingAs($employee01)15 ->get(route('tasks.show', $employee02->tasks->first()->id))16 ->assertForbidden();17});
Ejecutamos los tests con el comando.
1./vendor/bin/pest
Resultado
Wau son varios tests, pero nos servirá cuando conforme al tiempo vayamos añadiendo nuevas funcionalidades ya que al ejecutar los tests veremos si hemos roto alguna funcionalidad.
Recuerda que al ejecutar los tests, por esta oportunidad lo estamos haciendo en nuestra propia BD así que probablemente los registros se hayan borrado. Para llenar nuevamente nuestra aplicación de prueba podemos ejecutar:
1php artisan db:seed
Y eso es todo amigos 😁, espero te haya sido de utilidad este tutorial y no olvides compartirlo con tus colegas!
Abajo te dejo el link del repositorio con el tutorial completo (branch main).
Tendrás tutoriales, tips, conceptos y puedas convertirte en un artesano de todo el ecosistema Laravel.
Revisa los detalles del nuevo curso en desarrollo