Controlling what users can or cannot do in your application is one of the most essential things you'll need to do when building real-world applications.
For example, in a todo application, you don't want a user to be able to edit or delete other users' todos.
In this article, you will learn one of the seamless ways to do this in Laravel by using policies to control what users can do by building a simple todo application.
To follow along with this tutorial, you need to have a basic understanding of Laravel and its application structure.
Create a Base Application
Run the following command to create a new Laravel application in your desired folder and move into it:
composer create-project laravel/laravel todo-app && cd todo-app
Next, run the following command to install Laravel Breeze:
php artisan breeze:install
Breeze will scaffold your new application with authentication so your users can register, log in, log out, and view their personalized dashboards.
After that, compile your application assets by running the following commands:
npm install && npm run dev
Laravel comes with the file-based SQLite database by default, so the next thing you need to do is connect your application database file to a database viewer like TablePlus or any other one you like.
After connecting your database to the viewer, run the following commands to migrate the available tables into your database:
php artisan migrate
Once that is done, run the following command to view your application in the browser:
php artisan serve
You should now see your new Laravel application at localhost:8000
looking like this:
You can now go to the register page to create a user and access the dashboard, which is the entire application at this point.
Model Setup
Models in Laravel are used to control database tables. Use the following command to create a Todo
model in the App/Models
folder:
php artisan make:model Todo
Next, inside the newly created file, replace the Todo
class with the following code:
class Todo extends Model
{
use HasFactory;
protected $fillable = [
'title',
'description',
'completed',
'user_id'
];
public function user()
{
return $this->belongsTo(User::class);
}
}
The code above will enable users to submit a form with the $fillable
properties; it also defines the relationship between a user and a Todo; in this case, a todo belongs to a user. Let's complete the relationship setup by adding the following code to the App/Models/User.php
file:
public function todos()
{
return $this->hasMany(Todo::class);
}
The code above will connect the User
model to the Todo
model so that it can have many to-dos.
Migration Setup
Migrations in Laravel are used to specify what should be in a database table. Run the following command to create a migration inside the database/migrations
folder:
php artisan make:migration create_todos_table
Next, replace the up
function in the new file with the following that will add the todo table to the database with the id
, user_id
, title
, description
, completed
, and timestamp columns:
public function up(): void
{
Schema::create('todos', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('description')->nullable();
$table->boolean('completed')->default(false);
$table->timestamps();
});
}
Next, run the following command to add the todos
table to the database:
php artisan migrate
Policy Setup
Policies in Laravel allow you to define who can do what with a particular resource, in this case, todos.
Let's see how that works by generating a TodoPolicy
inside the App/Policies
folder using the following command:
php artisan make:policy TodoPolicy --model=Todo
Next, in the newly created TodoPolicy
file, replace the TodoPolicy
class with the following code:
class TodoPolicy
{
/**
* Determine if the user can view any todos.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine if the user can view the todo.
*/
public function view(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id;
}
/**
* Determine if the user can create todos.
*/
public function create(User $user): bool
{
return true;
}
/**
* Determine if the user can update the todo.
*/
public function update(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id;
}
/**
* Determine if the user can delete the todo.
*/
public function delete(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id;
}
}
The code above specifies that a user can create a todo, but can only view, update, or delete a todo that belongs to them.
Next, let's set up the controller in the next section.
Controller Setup
Controllers in Laravel control the app's functionality for a particular resource. Run the following command to generate a TodoController
inside the App/Http/Controllers
:
php artisan make:controller TodoController
Add the following code to the top of the newly created TodoController
file to import the Todo
model for database operations and Gate
class for authorization:
use App\Models\Todo;
use Illuminate\Support\Facades\Gate;
Index Method
Replace the index
method with the following code that fetches and returns all the logged-in users' todos:
public function index()
{
Gate::authorize('viewAny', Todo::class);
$todos = auth()->user()->todos;
return view('todos.index', compact('todos'));
}
The Gate::authorize
method verifies that the user is logged in using the viewAny
policy method you defined in the previous section.
Create Method
Replace the create
method with the following code that verifies the user is signed in before returning the create todo form to the user so they can create todos:
public function create()
{
Gate::authorize('create', Todo::class);
return view('todos.create');
}
Store Method
Replace the store
method with the following code that checks if the user can create a todo, validates the request, creates the todo, and redirects the user to the todo list page:
public function store(Request $request)
{
Gate::authorize('create', Todo::class);
$validated = $request->validate([
'title' => 'required|max:255',
'description' => 'nullable'
]);
$todo = auth()->user()->todos()->create($validated);
return redirect()->route('todos.index')
->with('success', 'Todo created successfully');
}
Edit Method
Replace the edit
method with the following code that verifies the user can edit that todo before returning the edit todo form populated with the selected todo to the user so they can edit it:
public function edit(Todo $todo)
{
Gate::authorize('update', $todo);
return view('todos.edit', compact('todo'));
}
Update Method
Replace the update
method with the following code that checks if the user can update the todo, validates the request, updates the selected todo, and redirects the user to the todo list page:
public function update(Request $request, Todo $todo)
{
Gate::authorize('update', $todo);
$validated = $request->validate([
'title' => 'required|max:255',
'description' => 'nullable'
]);
$todo->update($validated);
return redirect()->route('todos.index')
->with('success', 'Todo updated successfully');
}
Destroy Method
Replace the destroy
method with the following code that checks if the user can delete the todo, deletes it, and redirects the user to the todo list page:
public function destroy(Todo $todo)
{
Gate::authorize('delete', $todo);
$todo->delete();
return redirect()->route('todos.index')
->with('success', 'Todo deleted successfully');
}
Your TodoController
file should now look like this:
<?php
namespace App\Http\Controllers;
use App\Models\Todo;
use Illuminate\Support\Facades\Gate;
use Illuminate\Http\Request;
class TodoController extends Controller
{
public function __construct()
{
//
}
public function index()
{
Gate::authorize('viewAny', Todo::class);
$todos = auth()->user()->todos;
return view('todos.index', compact('todos'));
}
public function create()
{
Gate::authorize('create', Todo::class);
return view('todos.create');
}
public function store(Request $request)
{
Gate::authorize('create', Todo::class);
$validated = $request->validate([
'title' => 'required|max:255',
'description' => 'nullable'
]);
$todo = auth()->user()->todos()->create($validated);
return redirect()->route('todos.index')
->with('success', 'Todo created successfully');
}
public function edit(Todo $todo)
{
Gate::authorize('update', $todo);
return view('todos.edit', compact('todo'));
}
public function update(Request $request, Todo $todo)
{
Gate::authorize('update', $todo);
$validated = $request->validate([
'title' => 'required|max:255',
'description' => 'nullable'
]);
$todo->update($validated);
return redirect()->route('todos.index')
->with('success', 'Todo updated successfully');
}
public function destroy(Todo $todo)
{
Gate::authorize('delete', $todo);
$todo->delete();
return redirect()->route('todos.index')
->with('success', 'Todo deleted successfully');
}
}
Routes Setup
Handling routes for your TodoController
is relatively straightforward using the resource
method in Laravel. Do that by adding the following code to the end of the routes/web.php
folder like so:
// rest of the file
Route::middleware(['auth'])->group(function () {
Route::resource('todos', TodoController::class);
});
The code above uses the auth
middleware to protect the todos resource. You should now be able to visit the following routes in your application:
/todos
: List all users' todos/todos/create
: Shows the form for creating todos/todos/edit/1
: Shows the form for editing a todo with the given id; 1 in this case.
But wait, you need to create the corresponding views, so let's do that in the next section.
Views Setup
Now that your TodoController
file and routes are all set, you can now create the views for your applications by creating a new todos
folder inside the resources/views
folder. After that, create create.blade.php
, edit.blade.php
, index.blade.php
files in the new todos
folder.
Index View
Paste the following code inside the index.blade.php
:
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
{{ __('Todos') }}
</h2>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
{{-- <div class="mb-4">
<a href="{{ route('todos.create') }}"
class="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600">
Create New Todo
</a>
</div> --}}
<div class="mt-6">
@foreach($todos as $todo)
<div class="mb-4 p-4 border rounded">
<h3 class="text-lg font-semibold">{{ $todo->title }}</h3>
<p class="text-gray-600">{{ $todo->description }}</p>
<div class="mt-2">
<a href="{{ route('todos.edit', $todo) }}"
class="text-blue-500 hover:underline">Edit</a>
<form action="{{ route('todos.destroy', $todo) }}"
method="POST"
class="inline">
@csrf
@method('DELETE')
<button type="submit"
class="ml-2 text-red-500 hover:underline"
onclick="return confirm('Are you sure?')">
Delete
</button>
</form>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
</x-app-layout>
Create View
Paste the following code inside the create.blade.php
:
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
{{ __('Create Todo') }}
</h2>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<form action="{{ route('todos.store') }}" method="POST">
@csrf
<div class="mb-4">
<label for="title" class="block text-gray-700">Title</label>
<input type="text"
name="title"
id="title"
class="w-full px-3 py-2 border rounded"
required>
</div>
<div class="mb-4">
<label for="description" class="block text-gray-700">Description</label>
<textarea name="description"
id="description"
class="w-full px-3 py-2 border rounded"></textarea>
</div>
<div class="flex items-center">
<button type="submit"
class="px-4 py-2 text-white bg-green-500 rounded hover:bg-green-600">
Create Todo
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>
Edit View
Paste the following code inside the edit.blade.php
:
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
{{ __('Edit Todo') }}
</h2>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<form action="{{ route('todos.update', $todo) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-4">
<label for="title" class="block text-gray-700">Title</label>
<input type="text"
name="title"
id="title"
value="{{ $todo->title }}"
class="w-full px-3 py-2 border rounded"
required>
</div>
<div class="mb-4">
<label for="description" class="block text-gray-700">Description</label>
<textarea name="description"
id="description"
class="w-full px-3 py-2 border rounded">{{ $todo->description }}</textarea>
</div>
<div class="flex items-center">
<button type="submit"
class="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600">
Update Todo
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>
You can now create, edit, and delete todos, BUT only as a logged-in user and the owner of the selected todos in the case of editing and deleting.
Conclusion
And that's it! You have just created a realistic todo application that allows users to create, view, edit, and delete ONLY their own todos. Please let me know if you have any corrections, suggestions, or questions in the comments!
Finally, remember to follow me here on Dev, LinkedIn, and Twitter. Thank you so much for reading, and I'll see you in the next one!