Build a Task Manager with Laravel
Build a full-stack task manager app with PHP 8, Laravel 13, and SQLite.
Introduction
30 Second Summary
Everyone has a to-do list. Whether it's on a sticky note or in an app, managing tasks is something you do every single day.
In this project, you will build a task manager web application using PHP 8 and Laravel. You'll create tasks with titles, descriptions, and priority levels, then manage them with full create, view, edit, and delete functionality.
What You'll Build
A task manager web app powered by Laravel's MVC architecture, Eloquent ORM for the database layer, and Blade templates for the frontend.
By the end of this project, you'll have:
- A Laravel 13 application with routing, controllers, and Blade template views.
- A Task model backed by a SQLite database with a migration defining the schema.
- Full CRUD functionality (Create, Read, Update, Delete) with form validation and user-friendly error messages.
- Secret Mission: Add task categories with an Eloquent relationship so each category can hold many tasks.
Are there any prerequisites?
You only need a basic understanding of HTML. No prior PHP or Laravel experience is required.
Install PHP, Composer, and Laravel
To build a task manager with Laravel, you first need a working PHP development environment on your machine.
In this step, you'll install PHP and Composer using a single command, create a new Laravel project, and spin up the development server so you can see it running in your browser.
In this step, get ready to:
- Install PHP 8.5, Composer, and the Laravel installer.
- Create a new Laravel 13 project with SQLite as the database.
- Start the development server and confirm the app loads in your browser.
Install PHP and Composer via php.new
Laravel provides a one-command installer called php.new that installs PHP, Composer (PHP's package manager), and the Laravel installer all at once.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Run the install command for your operating system:
macOS
- Install PHP, Composer, and the Laravel installer by running this command:
/bin/bash -c "$(curl -fsSL https://php.new/install/mac/8.5)"
What does this command do?
This downloads and runs the official Laravel installer script from php.new. It installs PHP 8.5, Composer, and the laravel CLI tool as globally accessible binaries on your machine.
Windows
- Open PowerShell as Administrator by right-clicking the PowerShell icon and selecting Run as administrator.
- Install PHP, Composer, and the Laravel installer by running this command:
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://php.new/install/windows/8.5'))
What does this command do?
This temporarily bypasses PowerShell's execution policy for this session, then downloads and runs the official Laravel installer from php.new. It installs PHP 8.5, Composer, and the laravel CLI tool.
Linux
- Install PHP, Composer, and the Laravel installer by running this command:
/bin/bash -c "$(curl -fsSL https://php.new/install/linux/8.5)"
What does this command do?
This downloads and runs the official Laravel installer script from php.new. It installs PHP 8.5, Composer, and the laravel CLI tool as globally accessible binaries.
- Close and reopen your terminal after the installation finishes. This refreshes your terminal session so it can find the new commands.
- Verify PHP is installed by running:
php --version
- You should see output starting with PHP 8.5 followed by a version number.
- Verify Composer is installed by running:
composer --version
- You should see output showing a Composer version number.
Command not found?
If your terminal says php or composer is not found after reopening it, make sure you fully closed and reopened the terminal window. On macOS, try opening a brand new Terminal tab. On Windows, close all PowerShell windows and open a fresh one.
Help me troubleshoot why php or composer is not found after running the php.new installer.
Create a new Laravel project
Now that PHP and Composer are ready, you can use the Laravel installer to scaffold a brand new project. The installer will ask you a few questions about your preferences.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your new Laravel project by running:
laravel new task-manager
What prompts will I see?
The Laravel installer will ask you to choose a starter kit, a testing framework, and a database. When prompted for a starter kit, select None. You will build everything from scratch.
When prompted for the database, select SQLite. SQLite stores your entire database in a single file with zero configuration needed.
- When prompted for a starter kit, select None.
- When prompted for the database, select SQLite.
- Accept the defaults for any other prompts.
After the command finishes, Laravel has created a task-manager folder on your Desktop with all the project files inside. It also created the SQLite database file at database/database.sqlite and ran the initial migrations automatically.
Seeing an error during project creation?
If you see a permissions error, make sure you have write access to your Desktop folder. If the laravel command is not found, re-run the php.new install command from the previous substep and reopen your terminal.
Help me fix an error when running laravel new task-manager.
Start the development server
Your project is scaffolded and the database is ready. Time to spin up the local server and see Laravel running in your browser.
- Navigate into your project folder by running:
cd task-manager
- Start the Laravel development server by running:
composer run dev
- Open your browser and visit http://localhost:8000.
- You should see the default Laravel welcome page with the Laravel logo.
What does composer run dev do?
This command starts Laravel's built-in development server along with supporting services. Your app is served at http://localhost:8000 and automatically watches for changes.
Keep this terminal window open while you work. To stop the server later, press Ctrl+C in the terminal.
Page not loading at localhost:8000?
Make sure you ran composer run dev from inside the task-manager folder (not its parent). Check your terminal output for error messages. If port 8000 is already in use, close any other local servers you might have running.
Help me fix the Laravel dev server not loading at localhost:8000.
Your Laravel project is live and your database is ready. Next up, you'll create the Task model and define the database structure for your tasks.
Create the Task Model and Migration
Your Laravel project is set up and the development server is running. Now it's time to give your app something to manage: tasks.
Every database-backed app needs two things: a way to describe the data (the model) and a way to structure the database table (the migration). Laravel gives you both with a single Artisan command.
In this step, get ready to:
- Generate a Task model and migration file with Artisan.
- Define the database columns for your tasks table.
- Configure the model to safely accept form data.
Generate the model with migration
Laravel's Eloquent ORM maps PHP classes to database tables. Instead of writing raw SQL, you interact with your database through model objects.
- Open a new terminal window inside your task-manager folder.
- Generate the Task model and its migration file by running this command:
php artisan make:model Task --migration
What does this command do?
This single command creates two files for you:
- app/Models/Task.php is your Eloquent model class that represents a single task.
- A timestamped migration file in database/migrations/ that defines the tasks table structure.
You should see confirmation messages in your terminal indicating both files were created.
Command not found?
- Make sure you are inside the task-manager folder, not its parent directory.
- Confirm PHP is available by running php --version in the same terminal.
Help me troubleshoot the make:model command
Define the database schema
The migration file is like a blueprint for your database table. Right now it only has id and timestamps columns. You need to add the columns that describe a task.
- In your editor, open the database/migrations/ folder.
- Find the file ending in _create_tasks_table.php and open it.
- Inside the up() method, find the line $table->id(); and add these four lines directly below it:
$table->string('title');
$table->text('description')->nullable();
$table->string('priority')->default('medium');
$table->boolean('completed')->default(false);
What does each column do?
- string('title') stores the task name as a short text string.
- text('description')->nullable() holds an optional longer description. nullable() means this field can be left empty.
- string('priority')->default('medium') sets a default priority of medium if none is provided.
- boolean('completed')->default(false) tracks whether the task is done, defaulting to not completed.
- Save the migration file.
- Run the migration to create the tasks table in your database:
php artisan migrate
You should see output showing your migration running, followed by DONE.
Migration failed?
- Check for typos in your migration file. A missing semicolon or mismatched parenthesis will cause a syntax error.
- Make sure you placed the new lines between $table->id(); and $table->timestamps(); inside the Schema::create closure.
- If you see a "table already exists" error, run php artisan migrate:fresh to reset and re-run all migrations.
Help me fix my migration error
✔️ Awesome, I've got everything!
Great. Make sure you have saved the migration file.
ⓧ I'd like to double check the full code
Here is the complete migration file for comparison:
<?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('tasks', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description')->nullable();
$table->string('priority')->default('medium');
$table->boolean('completed')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tasks');
}
};
Configure the model for mass assignment
Laravel protects your database from unexpected input through a feature called mass assignment protection. You need to explicitly list which fields are allowed to be filled from form data.
- Open app/Models/Task.php in your editor.
- Replace the empty class body with the following $fillable property:
protected $fillable = [
'title',
'description',
'priority',
'completed',
];
What is mass assignment protection?
Without $fillable, Laravel blocks any attempt to create or update a model using an array of data. This prevents attackers from injecting unexpected fields into your database.
By listing title, description, priority, and completed in $fillable, you are telling Laravel these four fields are safe to accept from form submissions.
Now add the casts() method directly below the $fillable property. This tells Laravel to automatically convert the completed column to a true/false boolean in PHP:
protected function casts(): array
{
return [
'completed' => 'boolean',
];
}
Why use a casts() method?
Databases store booleans as 0 or 1. The casts() method tells Eloquent to convert that value to a proper PHP true or false when you read the model.
The : array return type on this method is a PHP 8 feature called a return type declaration. It ensures the method always returns an array.
- Save app/Models/Task.php.
Seeing a syntax error?
- Make sure both the $fillable property and casts() method are inside the class Task extends Model braces.
- Check that you have not accidentally deleted the opening <?php tag or the namespace line at the top of the file.
Help me fix my Task model
✔️ Awesome, I've got everything!
Great. Double check you have saved app/Models/Task.php.
ⓧ I'd like to double check the full code
Here is the complete Task.php model for comparison:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
protected $fillable = [
'title',
'description',
'priority',
'completed',
];
protected function casts(): array
{
return [
'completed' => 'boolean',
];
}
}
Your Task model and database table are ready. Next up, you'll build the controller that connects user requests to your model.
Build the Task Controller
Your Task model and database migration are in place. Now your app needs a way to respond when a user visits a URL, submits a form, or clicks a button.
In Laravel, a controller handles each incoming request. It fetches data from the model, processes form input, and decides what to show the user next. You will generate a resource controller that comes pre-loaded with methods for every CRUD action.
In this step, get ready to:
- Generate a resource controller with pre-stubbed CRUD methods.
- Register routes that map URLs to controller actions.
- Implement the controller methods for managing tasks.
Generate a resource controller
Laravel's Artisan CLI can scaffold a controller with all seven RESTful methods already stubbed out. The --resource flag adds the CRUD method stubs, and --model=Task wires up type-hinted parameters so Laravel automatically resolves the correct Task from the URL.
- Generate the TaskController by running this command in your terminal:
php artisan make:controller TaskController --resource --model=Task
What does this command do?
The make:controller Artisan command creates a new PHP class at app/Http/Controllers/TaskController.php.
The --resource flag adds seven empty methods: index, create, store, show, edit, update, and destroy. These follow Laravel's RESTful convention for handling common database operations.
- Confirm the file was created by running:
ls app/Http/Controllers/TaskController.php
You should see the file path printed back to you, confirming the controller exists.
Don't see the file?
Make sure you are running the command from inside the task-manager directory. If your terminal is elsewhere, run cd task-manager first.
Help me troubleshoot why my controller file wasn't created.
Register the resource route
Routes tell Laravel which controller method to call for each URL. A single resource route declaration registers all seven standard endpoints at once.
- Open routes/web.php in your editor.
- Replace the entire contents of the file with the following code:
<?php
use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;
// Redirect the homepage to the tasks list
Route::get('/', function () {
return redirect()->route('tasks.index');
});
// Register all 7 CRUD routes for tasks
Route::resource('tasks', TaskController::class);
// Custom route to toggle a task's completion status
Route::patch('/tasks/{task}/toggle', [TaskController::class, 'toggle'])->name('tasks.toggle');
What does this code do?
- The root / route redirects visitors to tasks.index so the homepage shows the task list immediately.
- Route::resource creates GET, POST, PUT, and DELETE endpoints mapped to each controller method (index, create, store, show, edit, update, destroy).
- The custom PATCH route lets users toggle a task's completion without a full edit form. The {task} segment triggers route model binding, which means Laravel automatically fetches the matching Task from the database.
- Save routes/web.php.
- Verify the routes registered correctly by running:
php artisan route:list --path=tasks
You should see a table listing routes like GET tasks, POST tasks, GET tasks/{task}, and the custom PATCH tasks/{task}/toggle route.
Seeing a class not found error?
Check that the use statement at the top of routes/web.php matches exactly: use App\Http\Controllers\TaskController;. A typo in the namespace will cause this error.
Help me fix my routes file.
✔️ Awesome, I've got everything!
Great. Double check you have saved routes/web.php before moving on.
ⓧ I'd like to double check the full code
<?php
use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return redirect()->route('tasks.index');
});
Route::resource('tasks', TaskController::class);
Route::patch('/tasks/{task}/toggle', [TaskController::class, 'toggle'])->name('tasks.toggle');
Implement the controller methods
The generated controller has empty method stubs. You need to fill them with the logic that fetches tasks, validates input, and redirects users after changes.
- Open app/Http/Controllers/TaskController.php in your editor.
- Replace the entire file contents with the following code:
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class TaskController extends Controller
{
// Display a list of all tasks, newest first
public function index(): View
{
$tasks = Task::latest()->get();
return view('tasks.index', compact('tasks'));
}
// Show the form for creating a new task
public function create(): View
{
return view('tasks.create');
}
// Validate input and save a new task to the database
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'priority' => 'required|in:low,medium,high',
]);
Task::create($validated);
return redirect()->route('tasks.index')
->with('success', 'Task created successfully.');
}
// Display a single task's full details
public function show(Task $task): View
{
return view('tasks.show', compact('task'));
}
// Show the form for editing an existing task
public function edit(Task $task): View
{
return view('tasks.edit', compact('task'));
}
// Validate input and update the task in the database
public function update(Request $request, Task $task): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'priority' => 'required|in:low,medium,high',
]);
$task->update($validated);
return redirect()->route('tasks.index')
->with('success', 'Task updated successfully.');
}
// Delete a task from the database
public function destroy(Task $task): RedirectResponse
{
$task->delete();
return redirect()->route('tasks.index')
->with('success', 'Task deleted successfully.');
}
// Toggle a task between complete and incomplete
public function toggle(Task $task): RedirectResponse
{
$task->update(['completed' => !$task->completed]);
$message = match($task->completed) {
true => 'Task marked as complete!',
false => 'Task marked as incomplete.',
};
return redirect()->route('tasks.index')
->with('success', $message);
}
}
What does this code do?
- index() fetches all tasks sorted by newest first using Task::latest(), then passes them to a Blade view.
- store() and update() validate input (title required, priority must be low/medium/high) before saving. If validation fails, Laravel redirects back with error messages automatically.
- show() and edit() accept a Task $task parameter. This uses route model binding so Laravel fetches the correct task from the database based on the URL.
- toggle() flips the completed boolean and uses PHP 8's match expression to pick the right success message.
Every method declares a return type (View or RedirectResponse). This is a PHP 8 feature called return type declarations that makes your code self-documenting.
What is route model binding?
When a method signature says Task $task, Laravel reads the {task} segment from the URL and automatically queries the database for a Task with that ID. If the task does not exist, Laravel returns a 404 page.
- Save app/Http/Controllers/TaskController.php.
- Visit http://localhost:8000 in your browser. You will see an error about a missing view file. This is expected because you have not created the Blade templates yet.
Seeing a 500 error instead of a missing view error?
Check that the use statements at the top of your controller match exactly. Missing the use App\Models\Task; import is the most common cause.
Also verify you did not accidentally delete the extends Controller part of the class declaration.
Help me debug my TaskController.
✔️ Awesome, I've got everything!
Great. Double check you have saved TaskController.php before moving on.
ⓧ I'd like to double check the full code
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class TaskController extends Controller
{
public function index(): View
{
$tasks = Task::latest()->get();
return view('tasks.index', compact('tasks'));
}
public function create(): View
{
return view('tasks.create');
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'priority' => 'required|in:low,medium,high',
]);
Task::create($validated);
return redirect()->route('tasks.index')
->with('success', 'Task created successfully.');
}
public function show(Task $task): View
{
return view('tasks.show', compact('task'));
}
public function edit(Task $task): View
{
return view('tasks.edit', compact('task'));
}
public function update(Request $request, Task $task): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'priority' => 'required|in:low,medium,high',
]);
$task->update($validated);
return redirect()->route('tasks.index')
->with('success', 'Task updated successfully.');
}
public function destroy(Task $task): RedirectResponse
{
$task->delete();
return redirect()->route('tasks.index')
->with('success', 'Task deleted successfully.');
}
public function toggle(Task $task): RedirectResponse
{
$task->update(['completed' => !$task->completed]);
$message = match($task->completed) {
true => 'Task marked as complete!',
false => 'Task marked as incomplete.',
};
return redirect()->route('tasks.index')
->with('success', $message);
}
}
Your controller and routes are wired up. Next, you will create the Blade templates that give your task manager a visual interface.
Create Blade Templates for Listing Tasks
Your controller is ready to fetch tasks and return views. But those views don't exist yet.
Laravel uses Blade as its templating engine. Blade lets you write HTML with special directives (like loops and conditionals) that keep your views clean and readable. In this step, you'll create the templates that display your tasks in the browser.
In this step, get ready to:
- Create a shared layout template with navigation and styling.
- Build the tasks index view that lists all tasks.
- Add a show view to display a single task's full details.
Create a layout template
Blade layouts let you define a shared HTML structure that all your pages inherit. Instead of repeating the same <head> and <nav> on every page, you define them once in a layout and each page fills in its unique content.
- In your editor's file explorer, create a new folder called layouts inside resources/views/.
- Inside that layouts folder, create a new file called app.blade.php.
- Paste the following code into resources/views/layouts/app.blade.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager</title>
<!-- Load Tailwind CSS from CDN for styling -->
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<!-- Navigation bar -->
<nav class="bg-white shadow-sm">
<div class="max-w-4xl mx-auto px-4 py-4">
<a href="{{ route('tasks.index') }}" class="text-xl font-bold text-gray-800">Task Manager</a>
</div>
</nav>
<main class="max-w-4xl mx-auto px-4 py-8">
<!-- Display flash success messages -->
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-6">
{{ session('success') }}
</div>
@endif
<!-- Each page fills this section with its own content -->
@yield('content')
</main>
</body>
</html>
What does this template do?
This layout defines the shared HTML skeleton for every page in your app.
- The <script src="https://cdn.tailwindcss.com"> tag loads Tailwind CSS for styling without needing a Node.js build step.
- The @if(session('success')) block checks for flash messages and displays them as a green banner.
- The @yield('content') directive is where child templates inject their unique content.
- Save app.blade.php.
Why Tailwind via CDN?
The CDN version of Tailwind avoids adding Node.js and npm to this project. It keeps the focus on PHP and Laravel. The CDN is designed for development and learning, which is exactly what you need here.
Build the tasks index view
The index view is the main page of your app. It lists every task with its title, priority level, and a toggle button for completion status.
- Inside resources/views/, create a new folder called tasks.
- Inside that tasks folder, create a new file called index.blade.php.
- Paste the following code into resources/views/tasks/index.blade.php:
@extends('layouts.app')
@section('content')
<!-- Page header with title and create button -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">My Tasks</h1>
<a href="{{ route('tasks.create') }}" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
New Task
</a>
</div>
<!-- Loop through tasks, or show empty state -->
@forelse($tasks as $task)
<div class="bg-white rounded-lg shadow-sm p-4 mb-3 flex items-center justify-between">
<div class="flex items-center gap-4">
<!-- Toggle completion form -->
<form action="{{ route('tasks.toggle', $task) }}" method="POST">
@csrf
@method('PATCH')
<button type="submit" class="w-6 h-6 rounded-full border-2 flex items-center justify-center {{ $task->completed ? 'bg-green-500 border-green-500' : 'border-gray-300' }}">
@if($task->completed)
<span class="text-white text-sm">✓</span>
@endif
</button>
</form>
<div>
<!-- Task title links to show page -->
<a href="{{ route('tasks.show', $task) }}" class="font-medium {{ $task->completed ? 'line-through text-gray-400' : 'text-gray-800' }}">
{{ $task->title }}
</a>
<!-- Priority badge with color coding -->
<span class="ml-2 text-xs px-2 py-1 rounded {{ $task->priority === 'high' ? 'bg-red-100 text-red-800' : ($task->priority === 'medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800') }}">
{{ ucfirst($task->priority) }}
</span>
</div>
</div>
<div class="flex gap-2">
<a href="{{ route('tasks.edit', $task) }}" class="text-blue-500 hover:text-blue-700 text-sm">Edit</a>
<!-- Delete form with confirmation -->
<form action="{{ route('tasks.destroy', $task) }}" method="POST" onsubmit="return confirm('Delete this task?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-500 hover:text-red-700 text-sm">Delete</button>
</form>
</div>
</div>
@empty
<div class="bg-white rounded-lg shadow-sm p-8 text-center text-gray-500">
No tasks yet. Create your first task!
</div>
@endforelse
@endsection
What does this view do?
This template extends your layout and fills the @yield('content') section with the task list.
- @forelse loops through $tasks and shows an empty state message if there are none.
- Each task card has a circular toggle button that submits a PATCH form to mark the task complete or incomplete.
- The priority badge uses conditional Tailwind classes to colour-code red, yellow, or green.
- @csrf adds a hidden CSRF token field that Laravel requires on all form submissions for security.
- Save index.blade.php.
- Return to your browser and visit http://localhost:8000. You should see the "No tasks yet. Create your first task!" empty state message on a white card.
Seeing an error instead of the empty state?
- Check that the folder structure matches exactly: resources/views/layouts/app.blade.php and resources/views/tasks/index.blade.php. Laravel resolves view names from folder paths.
- Make sure both files are saved. Unsaved files won't be picked up by the server.
- Confirm your dev server is still running. If it stopped, run composer run dev again from the task-manager folder.
Still stuck? Ask for help.
Add the show view
When a user clicks a task title on the index page, they'll land on the show view. This page displays the full task details along with action buttons.
- Inside the resources/views/tasks/ folder, create a new file called show.blade.php.
- Paste the following code into resources/views/tasks/show.blade.php:
@extends('layouts.app')
@section('content')
<div class="bg-white rounded-lg shadow-sm p-6">
<!-- Task title and priority badge -->
<div class="flex justify-between items-start mb-4">
<h1 class="text-2xl font-bold text-gray-800">{{ $task->title }}</h1>
<span class="text-xs px-2 py-1 rounded {{ $task->priority === 'high' ? 'bg-red-100 text-red-800' : ($task->priority === 'medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800') }}">
{{ ucfirst($task->priority) }}
</span>
</div>
<!-- Task description with fallback text -->
<p class="text-gray-600 mb-4">{{ $task->description ?? 'No description provided.' }}</p>
<!-- Completion status -->
<p class="text-sm text-gray-500 mb-6">
Status: <span class="font-medium">{{ $task->completed ? 'Completed' : 'Pending' }}</span>
</p>
<!-- Action buttons -->
<div class="flex gap-3">
<a href="{{ route('tasks.edit', $task) }}" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">Edit</a>
<form action="{{ route('tasks.destroy', $task) }}" method="POST" onsubmit="return confirm('Delete this task?')">
@csrf
@method('DELETE')
<button type="submit" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded">Delete</button>
</form>
<a href="{{ route('tasks.index') }}" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded">Back</a>
</div>
</div>
@endsection
What does the show view do?
- Displays the task title, description (or a fallback message if empty), and completion status.
- The ?? operator is PHP's null coalescing operator. It shows the description if one exists, or the fallback string if it's null.
- The delete button uses @method('DELETE') because HTML forms only support GET and POST. This tells Laravel to treat the submission as a DELETE request.
- Save show.blade.php.
You won't be able to test the show view yet because there are no tasks in the database to click on. The next step builds the create form that lets you add tasks.
File not found errors?
- Double-check that your file is named show.blade.php (not show.php). The .blade.php extension is required for Laravel to process Blade directives.
- Verify the file is inside resources/views/tasks/ and not accidentally at the top level of resources/views/.
Need more help debugging?
✔️ Awesome, I've got everything!
Great. Make sure all three Blade files are saved before moving on.
ⓧ I'd like to double check the full code
Here are the three Blade files in full. Compare yours against these.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<nav class="bg-white shadow-sm">
<div class="max-w-4xl mx-auto px-4 py-4">
<a href="{{ route('tasks.index') }}" class="text-xl font-bold text-gray-800">Task Manager</a>
</div>
</nav>
<main class="max-w-4xl mx-auto px-4 py-8">
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-6">
{{ session('success') }}
</div>
@endif
@yield('content')
</main>
</body>
</html>
@extends('layouts.app')
@section('content')
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">My Tasks</h1>
<a href="{{ route('tasks.create') }}" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
New Task
</a>
</div>
@forelse($tasks as $task)
<div class="bg-white rounded-lg shadow-sm p-4 mb-3 flex items-center justify-between">
<div class="flex items-center gap-4">
<form action="{{ route('tasks.toggle', $task) }}" method="POST">
@csrf
@method('PATCH')
<button type="submit" class="w-6 h-6 rounded-full border-2 flex items-center justify-center {{ $task->completed ? 'bg-green-500 border-green-500' : 'border-gray-300' }}">
@if($task->completed)
<span class="text-white text-sm">✓</span>
@endif
</button>
</form>
<div>
<a href="{{ route('tasks.show', $task) }}" class="font-medium {{ $task->completed ? 'line-through text-gray-400' : 'text-gray-800' }}">
{{ $task->title }}
</a>
<span class="ml-2 text-xs px-2 py-1 rounded {{ $task->priority === 'high' ? 'bg-red-100 text-red-800' : ($task->priority === 'medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800') }}">
{{ ucfirst($task->priority) }}
</span>
</div>
</div>
<div class="flex gap-2">
<a href="{{ route('tasks.edit', $task) }}" class="text-blue-500 hover:text-blue-700 text-sm">Edit</a>
<form action="{{ route('tasks.destroy', $task) }}" method="POST" onsubmit="return confirm('Delete this task?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-500 hover:text-red-700 text-sm">Delete</button>
</form>
</div>
</div>
@empty
<div class="bg-white rounded-lg shadow-sm p-8 text-center text-gray-500">
No tasks yet. Create your first task!
</div>
@endforelse
@endsection
@extends('layouts.app')
@section('content')
<div class="bg-white rounded-lg shadow-sm p-6">
<div class="flex justify-between items-start mb-4">
<h1 class="text-2xl font-bold text-gray-800">{{ $task->title }}</h1>
<span class="text-xs px-2 py-1 rounded {{ $task->priority === 'high' ? 'bg-red-100 text-red-800' : ($task->priority === 'medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800') }}">
{{ ucfirst($task->priority) }}
</span>
</div>
<p class="text-gray-600 mb-4">{{ $task->description ?? 'No description provided.' }}</p>
<p class="text-sm text-gray-500 mb-6">
Status: <span class="font-medium">{{ $task->completed ? 'Completed' : 'Pending' }}</span>
</p>
<div class="flex gap-3">
<a href="{{ route('tasks.edit', $task) }}" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">Edit</a>
<form action="{{ route('tasks.destroy', $task) }}" method="POST" onsubmit="return confirm('Delete this task?')">
@csrf
@method('DELETE')
<button type="submit" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded">Delete</button>
</form>
<a href="{{ route('tasks.index') }}" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded">Back</a>
</div>
</div>
@endsection
Your views are wired up and the index page renders in the browser. Next up, you'll build the create form so you can actually add tasks to your database and see them appear on this page.
Implement Create and Store
In the previous step, you built Blade templates that display tasks. Your index view lists all tasks, and your show view reveals a single task's details.
There's one problem: you have no way to actually create tasks yet. In this step, you'll build a form that collects task data from the user, validate the input on the server side, and display helpful error messages when something goes wrong.
In this step, get ready to:
- Build a create form with fields for title, description, and priority.
- Understand how Laravel validates and stores form submissions.
- Display validation errors when users submit invalid data.
Build the create form
Your index view already has a New Task button that links to the create route. You need to build the Blade template it points to.
- Create a new file at resources/views/tasks/create.blade.php and add this form structure:
@extends('layouts.app')
@section('content')
<h1 class="text-2xl font-bold text-gray-800 mb-6">Create New Task</h1>
<form action="{{ route('tasks.store') }}" method="POST" class="bg-white rounded-lg shadow-sm p-6">
@csrf
<div class="mb-4">
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Title</label>
<input type="text" name="title" id="title" value="{{ old('title') }}"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<div class="mb-4">
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea name="description" id="description" rows="3"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('description') }}</textarea>
</div>
<div class="mb-6">
<label for="priority" class="block text-sm font-medium text-gray-700 mb-1">Priority</label>
<select name="priority" id="priority"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="low" {{ old('priority') === 'low' ? 'selected' : '' }}>Low</option>
<option value="medium" {{ old('priority', 'medium') === 'medium' ? 'selected' : '' }}>Medium</option>
<option value="high" {{ old('priority') === 'high' ? 'selected' : '' }}>High</option>
</select>
</div>
<div class="flex gap-3">
<button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">Create Task</button>
<a href="{{ route('tasks.index') }}" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded">Cancel</a>
</div>
</form>
@endsection
What does this code do?
- @csrf adds a hidden token field that protects your form from cross-site request forgery attacks. Laravel rejects any POST request without a valid token.
- old('title') repopulates form fields with the user's previous input if validation fails. Without this, the form would clear all fields on error.
- The priority select defaults to "Medium" using old('priority', 'medium') which returns 'medium' when no previous input exists.
- The form's action points to route('tasks.store') which maps to the store() method in your controller.
- Save create.blade.php.
- In your browser, visit http://localhost:8000/tasks/create. You should see a white form card with Title, Description, and Priority fields plus a blue Create Task button.
Seeing a blank page or error?
- Make sure the file is saved at exactly resources/views/tasks/create.blade.php. The tasks folder must already exist from the previous step.
- Check that the first line says @extends('layouts.app') with no typos in the path.
Help me debug why my create form isn't rendering.
Understand the store method with validation
When a user submits the form, Laravel sends a POST request to the store() method in your controller. This method already exists from Step 3. Let's examine how it handles the form data.
- Open app/Http/Controllers/TaskController.php and find the store method:
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'priority' => 'required|in:low,medium,high',
]);
Task::create($validated);
return redirect()->route('tasks.index')
->with('success', 'Task created successfully.');
}
How does validation work here?
The $request->validate() method checks the incoming data against the rules you define. If any rule fails, Laravel automatically redirects the user back to the form with error messages.
- 'required|string|max:255' means the title must be present, must be a string, and cannot exceed 255 characters.
- 'nullable|string' means the description is optional but must be a string if provided.
- 'required|in:low,medium,high' means the priority must be one of those three exact values.
- Task::create($validated) saves only the validated data to the database. Anything else in the request is ignored.
After saving, the method redirects to the tasks index with a flash message stored in the session. Your layout template already displays this message using the session('success') check.
- Test the form by visiting http://localhost:8000/tasks/create in your browser.
- Enter Buy groceries as the title.
- Leave the description empty.
- Select High from the priority dropdown.
- Click Create Task.
You should be redirected to the task index page with a green "Task created successfully." banner and your new task listed below it.
Task not appearing after submission?
- Make sure your Task model has 'title', 'description', and 'priority' listed in the $fillable array. Without this, Laravel blocks mass assignment silently.
- Confirm your development server is still running. If it stopped, restart it with composer run dev.
Help me figure out why my task isn't being saved to the database.
Display validation errors
When validation fails, Laravel redirects back to the form with error messages. You need to display those messages next to the relevant fields so users know what to fix.
- In create.blade.php, find the closing > of the title <input> tag (the line that says required>).
- Add this error display block directly below it:
@error('title')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
- Find the closing </textarea> tag for the description field.
- Add this error display block directly below it:
@error('description')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
- Find the closing </select> tag for the priority field.
- Add this error display block directly below it:
@error('priority')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
What does @error do?
The @error('title') directive checks if a validation error exists for that specific field. If it does, the block inside renders with $message containing the error text (like "The title field is required.").
When validation passes, @error blocks are completely hidden. They only appear when something goes wrong.
- Save create.blade.php.
- Visit http://localhost:8000/tasks/create in your browser.
- Clear the title field (remove any text) and click Create Task.
You should see a red error message appear below the title field saying the title is required.
Not seeing error messages?
- The browser's HTML5 required attribute may prevent form submission before Laravel's server-side validation runs. Temporarily remove required from the title input to test, then add it back.
- Make sure each @error block is placed inside its parent <div> but after the input element. If it's outside the div, it may render in the wrong place.
Help me figure out why my validation error messages aren't displaying.
✔️ Awesome, I've got everything!
Great. Double check you've saved create.blade.php with all three @error blocks in place.
ⓧ I'd like to double check the full code
@extends('layouts.app')
@section('content')
<h1 class="text-2xl font-bold text-gray-800 mb-6">Create New Task</h1>
<form action="{{ route('tasks.store') }}" method="POST" class="bg-white rounded-lg shadow-sm p-6">
@csrf
<div class="mb-4">
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Title</label>
<input type="text" name="title" id="title" value="{{ old('title') }}"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
@error('title')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div class="mb-4">
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea name="description" id="description" rows="3"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('description') }}</textarea>
@error('description')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div class="mb-6">
<label for="priority" class="block text-sm font-medium text-gray-700 mb-1">Priority</label>
<select name="priority" id="priority"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="low" {{ old('priority') === 'low' ? 'selected' : '' }}>Low</option>
<option value="medium" {{ old('priority', 'medium') === 'medium' ? 'selected' : '' }}>Medium</option>
<option value="high" {{ old('priority') === 'high' ? 'selected' : '' }}>High</option>
</select>
@error('priority')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div class="flex gap-3">
<button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">Create Task</button>
<a href="{{ route('tasks.index') }}" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded">Cancel</a>
</div>
</form>
@endsection
Your task manager can now create tasks with full server-side validation. Next up, you'll build the edit and update functionality so users can modify existing tasks and toggle their completion status.
Implement Edit, Update, and Delete
You can create tasks and view them. Now you need a way to change tasks after they're created and remove tasks you no longer need.
In this step, you'll build the edit form and walk through the controller methods that handle updating, deleting, and toggling task completion. These methods are already in your controller from the resource generator. The missing piece is the edit view itself.
In this step, get ready to:
- Build the edit form with pre-filled values and form method spoofing.
- Walk through the update and destroy controller methods.
- Test the toggle completion feature powered by PHP 8's match expression.
Build the edit form
HTML forms only support GET and POST requests natively. To send a PUT request (which Laravel expects for updates), you need to "spoof" the method using a hidden field. Laravel's @method('PUT') Blade directive handles this for you.
- Create a new file at resources/views/tasks/edit.blade.php and paste the following code:
@extends('layouts.app')
@section('content')
<h1 class="text-2xl font-bold text-gray-800 mb-6">Edit Task</h1>
{{-- Form sends a POST request but spoofs PUT for Laravel's router --}}
<form action="{{ route('tasks.update', $task) }}" method="POST" class="bg-white rounded-lg shadow-sm p-6">
@csrf
@method('PUT')
{{-- Title field pre-filled with old input or existing task title --}}
<div class="mb-4">
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Title</label>
<input type="text" name="title" id="title" value="{{ old('title', $task->title) }}"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
@error('title')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
{{-- Description textarea pre-filled with existing value --}}
<div class="mb-4">
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea name="description" id="description" rows="3"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('description', $task->description) }}</textarea>
@error('description')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
{{-- Priority dropdown pre-selects the task's current priority --}}
<div class="mb-6">
<label for="priority" class="block text-sm font-medium text-gray-700 mb-1">Priority</label>
<select name="priority" id="priority"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="low" {{ old('priority', $task->priority) === 'low' ? 'selected' : '' }}>Low</option>
<option value="medium" {{ old('priority', $task->priority) === 'medium' ? 'selected' : '' }}>Medium</option>
<option value="high" {{ old('priority', $task->priority) === 'high' ? 'selected' : '' }}>High</option>
</select>
@error('priority')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div class="flex gap-3">
<button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">Update Task</button>
<a href="{{ route('tasks.index') }}" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded">Cancel</a>
</div>
</form>
@endsection
What does this template do?
This form is nearly identical to the create form, with three key differences:
- @method('PUT') tells Laravel to treat this POST request as a PUT request, which routes it to the update() controller method.
- Each field uses old('field', $task->field) to pre-fill with the task's current value. If validation fails, old() shows what the user last typed instead.
- The form action points to route('tasks.update', $task) which generates the URL /tasks/{id} for the specific task being edited.
- Save edit.blade.php.
- In your browser, go to http://localhost:8000 and click Edit on any task. You should see a form pre-filled with that task's title, description, and priority.
Seeing a 404 or blank page?
- Make sure the file is saved at exactly resources/views/tasks/edit.blade.php. The folder structure must match what the controller expects.
- Check that you have at least one task created. If your task list is empty, create a task first so you have something to edit.
- Confirm your development server is still running. If not, run composer run dev again from the task-manager folder.
Why isn't my edit form showing up when I click Edit?
Walk through update and destroy
Your TaskController already has the update() and destroy() methods fully implemented from when you generated the resource controller. Let's look at how they work.
- In app/Http/Controllers/TaskController.php, find the update() method:
public function update(Request $request, Task $task): RedirectResponse
{
// Validate the incoming data with the same rules as store
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'priority' => 'required|in:low,medium,high',
]);
// Update only the validated fields on the existing task
$task->update($validated);
// Redirect back to the task list with a success message
return redirect()->route('tasks.index')
->with('success', 'Task updated successfully.');
}
How does update() work?
Laravel's route model binding automatically fetches the correct task from the database based on the URL parameter. The $task variable is already a full Eloquent model instance when it arrives in the method.
The validate() call applies the same rules as when creating a task. If validation fails, Laravel automatically redirects back to the edit form with error messages. If it passes, $task->update() saves only the validated fields to the database.
- Now find the destroy() method just below it:
public function destroy(Task $task): RedirectResponse
{
// Remove the task from the database permanently
$task->delete();
// Redirect to the task list with confirmation
return redirect()->route('tasks.index')
->with('success', 'Task deleted successfully.');
}
How does destroy() work?
The destroy() method receives the task via route model binding (same as update). Calling $task->delete() removes the record from the database permanently.
If you look back at your index.blade.php, the delete button uses @method('DELETE') to spoof the DELETE HTTP verb. It also has an onsubmit="return confirm('Delete this task?')" attribute that shows a confirmation dialog before actually submitting.
- Test the update flow: click Edit on a task, change the title, and click Update Task. You should see the updated title on the task list with a green success message.
- Test the delete flow: click Delete on a task, confirm the dialog, and verify the task disappears from the list.
Update not saving your changes?
- Make sure your edit form has both @csrf and @method('PUT') directives. Without @method('PUT'), Laravel treats it as a plain POST request and won't route it to update().
- Check that the form action includes $task in the route helper: route('tasks.update', $task). Without it, Laravel doesn't know which task to update.
Why aren't my task edits saving to the database?
Test the toggle completion feature
Your task list already has circular toggle buttons next to each task. These buttons submit a PATCH request to a dedicated toggle() method that flips the task's completion status.
- In TaskController.php, find the toggle() method at the bottom of the file:
public function toggle(Task $task): RedirectResponse
{
// Flip the completed status (true becomes false, false becomes true)
$task->update(['completed' => !$task->completed]);
// PHP 8 match expression selects the message based on the new status
$message = match($task->completed) {
true => 'Task marked as complete!',
false => 'Task marked as incomplete.',
};
return redirect()->route('tasks.index')
->with('success', $message);
}
What is a match expression?
The match expression is a PHP 8 feature that works like a compact switch statement. It compares a value against multiple arms and returns the result of the matching arm.
Unlike switch, match uses strict comparison (===), doesn't fall through to the next case, and is an expression (it returns a value you can assign to a variable). Here it picks the right success message depending on whether the task is now complete or incomplete.
- In your browser, click the circle button next to any task. The circle should fill green with a checkmark, and you should see a "Task marked as complete" success message.
- Click the same circle again. The task should toggle back to incomplete with a strikethrough removed from the title.
Toggle button not working?
- Make sure your development server is running. The toggle sends a PATCH request, which requires the server to process it.
- Check that routes/web.php has the toggle route: Route::patch('/tasks/{task}/toggle', ...). This route must come after the Route::resource line.
Why is my toggle button not changing the task's completion status?
✔️ Awesome, I've got everything!
Great. Double check you've saved edit.blade.php and that edit, update, delete, and toggle are all working in the browser.
ⓧ I'd like to double check the full code
Here is the complete edit.blade.php file:
@extends('layouts.app')
@section('content')
<h1 class="text-2xl font-bold text-gray-800 mb-6">Edit Task</h1>
<form action="{{ route('tasks.update', $task) }}" method="POST" class="bg-white rounded-lg shadow-sm p-6">
@csrf
@method('PUT')
<div class="mb-4">
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Title</label>
<input type="text" name="title" id="title" value="{{ old('title', $task->title) }}"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
@error('title')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div class="mb-4">
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea name="description" id="description" rows="3"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('description', $task->description) }}</textarea>
@error('description')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div class="mb-6">
<label for="priority" class="block text-sm font-medium text-gray-700 mb-1">Priority</label>
<select name="priority" id="priority"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="low" {{ old('priority', $task->priority) === 'low' ? 'selected' : '' }}>Low</option>
<option value="medium" {{ old('priority', $task->priority) === 'medium' ? 'selected' : '' }}>Medium</option>
<option value="high" {{ old('priority', $task->priority) === 'high' ? 'selected' : '' }}>High</option>
</select>
@error('priority')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div class="flex gap-3">
<button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">Update Task</button>
<a href="{{ route('tasks.index') }}" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded">Cancel</a>
</div>
</form>
@endsection
And here is the complete TaskController.php with all methods:
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class TaskController extends Controller
{
public function index(): View
{
$tasks = Task::latest()->get();
return view('tasks.index', compact('tasks'));
}
public function create(): View
{
return view('tasks.create');
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'priority' => 'required|in:low,medium,high',
]);
Task::create($validated);
return redirect()->route('tasks.index')
->with('success', 'Task created successfully.');
}
public function show(Task $task): View
{
return view('tasks.show', compact('task'));
}
public function edit(Task $task): View
{
return view('tasks.edit', compact('task'));
}
public function update(Request $request, Task $task): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'priority' => 'required|in:low,medium,high',
]);
$task->update($validated);
return redirect()->route('tasks.index')
->with('success', 'Task updated successfully.');
}
public function destroy(Task $task): RedirectResponse
{
$task->delete();
return redirect()->route('tasks.index')
->with('success', 'Task deleted successfully.');
}
public function toggle(Task $task): RedirectResponse
{
$task->update(['completed' => !$task->completed]);
$message = match($task->completed) {
true => 'Task marked as complete!',
false => 'Task marked as incomplete.',
};
return redirect()->route('tasks.index')
->with('success', $message);
}
}
Your task manager now has full CRUD functionality. You can create, read, update, and delete tasks, plus toggle their completion status with a single click. That covers everything a task manager needs to be useful.
Secret mission
Add Task Categories
Your task manager works great, but real projects need organization. Add a Category model with an Eloquent relationship so each task can belong to a category like Work, Personal, or Shopping. This teaches you one of the most important Laravel skills: defining and using model relationships.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally with SQLite, so there are no ongoing costs or cloud resources to worry about.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The task-manager project folder on your Desktop.
- The Laravel development server running at http://localhost:8000.
- The SQLite database file inside the project folder.
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with your task manager app.
- Your development server stays running at http://localhost:8000.
- Your task-manager project folder stays on your Desktop.
- PHP, Composer, and the Laravel installer remain installed on your machine. These take minimal disk space and are useful for future Laravel projects.
Pause - I'll come back to this later
Shut down the running development server to free up system resources. Your project files stay on disk for next time.
- Switch to the terminal where composer run dev is running.
- Press Ctrl+C to stop the development server.
- When you want to resume, navigate back to the task-manager folder and run composer run dev again.
Delete - I don't want to use this again
Remove the project folder and all its contents. This deletes your task manager app, the SQLite database, and all task data permanently.
- Switch to the terminal where composer run dev is running and press Ctrl+C to stop the server.
- Delete the project folder by running this command (macOS/Linux):
cd ~/Desktop
rm -rf task-manager
On Windows, run this in PowerShell instead:
cd ~\Desktop
Remove-Item -Recurse -Force task-manager
- Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows) on your Desktop. You should no longer see task-manager in the output.
Nice Work!
Nice Work!
Nice work! You just built a full-stack task manager web application with PHP 8 and Laravel 13.
You've learned how to:
- Built a Laravel 13 application from scratch with routing, controllers, and Blade templates.
- Created an Eloquent ORM model with a database migration and full CRUD operations.
- Used PHP 8 features including typed properties, return types, named arguments, and the match expression in a real project.
- Added Eloquent relationships with task categories using hasMany and belongsTo.
Ready to quiz yourself?