Building a User-Based Task List Application in Laravel

Overview

In this article we will be building a task list application in Laravel 5.6 complete with a paginated list of tasks per user account, user registration, and password resets.

screenshot of the application we are building

There are a few things you should be familiar with already in order to get the most out of this article.

  • You should be familiar with PHP. I recommend PHP: The Right Way for a quick start.
  • You should be comfortable using the terminal. If you’re on Windows I recommend installing git for Windows.
  • You have a Laravel development environment set up. I recommend using the Vagrant box Homestead.

To begin, let’s define the basic functionality of our application:

  • a user can register for an account
  • a user can reset their password via an email link
  • a user can log in to their account
  • tasks are private to a user account
  • a user can create new tasks
  • a user can see a paginated list of their tasks with complete tasks at the end
  • a user can mark a task as complete
  • a user can log out of their account

Below is a basic schema of our application’s database. A data type in bold means not nullable. PK stands for Primary Key and FK stands for Foreign Key.

basic schema of our application’s database

Environment Configuration

Let’s get started by configuring Homestead and provisioning the Vagrant box.

If you’re not using Homestead, skip this part and set up your local environment for a new Laravel project as needed.

Edit your Homestead.yaml configuration file and add an entry under sites for our project. If you plan on creating the project in a directory other than the vagrant user’s home directory, be sure to update the file path to where you’ll be creating the project.

- map: task-app.test  
to: /home/vagrant/task-app/public

We also need to edit our hosts file and add the domain of our local site. On Max and Linux this is located at /etc/hosts and on Windows at C:\Windows\System32\drivers\etc\hosts.

Add the following entry to your hosts file:

192.168.10.10 task-app.test

Note that the local IP address 192.168.10.10 matches the configured IP address at the top of our Homestead.yaml file.

Now we need to provision our Vagrant box so Homestead can serve our newly configured site.

$ vagrant reload --provision

After Vagrant is finished provisioning the virtual machine ssh into the box.

$ vagrant ssh

Project Setup

Now it’s time to create our project using Composer, a dependency manager for PHP. This will generate a new Laravel application using the latest stable release into the directory task-app.

$ composer create-project --prefer-dist laravel/laravel task-app

Before we start coding let’s update our Environment Configuration with our application’s name and URL. Edit the top of the .env file at the root of the project directory with the updated APP_NAME and APP_URL.

APP_NAME="Task App"
APP_ENV=local
APP_KEY=base64:YOUR_APP_KEY
APP_DEBUG=true
APP_URL=http://task-app.test

A few lines down in the same file, enter your mailtrap credentials. Mailtrap.io is a great way to test emails locally without needing an actual email server (and without actually sending emails). It works by acting like an email server and showing you an outbox of emails that would have been sent.

Configure your .env file with your mailtrap username and password, optionally specifying the value tls for MAIL_ENCRYPTION to use the TLS protocol when sending emails. Let’s also specify a MAIL_FROM_NAME and MAIL_FROM_ADDRESS for sending emails. Laravel looks for these environment variables out of the box.

MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=**************
MAIL_PASSWORD=**************
MAIL_ENCRYPTION=tls
MAIL_FROM_NAME="Task App"
MAIL_FROM_ADDRESS=no-reply@task-app.test

Great! Next let’s touch up the welcome view by editing the resources/views/welcome.blade.php file.

Edit the contents of the title tag to contain {{ config('app.name') }}. This will insert the value of APP_NAME from our environment file. The string 'app.name' tells the config() helper function to look in the file config/app.php for an array key name and return the value. Take a look at the contents of the config directory to learn more about Laravel configuration.

Edit the title tag to look like:

<title>{{ config('app.name') }}</title>

Additionally in the same view, edit the div with class="content" by removing the links and editing the blade template to display our application name.

<div class="content">
<div class="title m-b-md">
{{ config('app.name') }}
</div>
</div>

You should now be able to navigate to task-app.test in your browser and see the title of our application!


Some Personal Preferences

Now for some personal preferences; things I like to do at the beginning of every project.

  1. Eloquent models go in the app/Models directory. This keeps our project structure tidy even if we have dozens of models (as opposed to putting them in the app directory which Laravel does by default).
  2. Password encryption is handled by a mutator on the User model. By using a mutator to encrypt the password we centralize where encryption happens, making it easier to swap out the implementation later if we desire. We also never have to remember to encrypt the password when creating a new User , just set the password attribute and the mutator takes care of the rest!
  3. Give the password_resets table a primary key. In my opinion having a primary key on every table is a best practice so let’s add one to the password_resets migration Laravel provides.

Let’s start by creating a new directory at app/Models.

$ mkdir app/Models

Move the User model into the new directory.

$ mv app/User.php app/Models/

Next let’s open up app/Models/User.php. Start by updating the namespace at the top to match our directory structure.

namespace App\Models;

While we’re editing this file let’s add the mutator method which encrypts the given value before setting it on the password attribute. The method naming convention setSomeAttribute where Some is the name of your attribute will register a mutator with Laravel.

/**
* Set and encrypt the password attribute.
*
*
@param $value
*/
public function setPasswordAttribute($value)
{
$this->attributes['password'] = bcrypt($value);
}

As mentioned earlier, we no longer need to encrypt the value before assigning a password, the mutator will do this for us. With that in mind, there are a couple of places we need to update in order for things to work properly.

Open up the UserFactory at database/factories/UserFactory and edit the contents to match below. We need to update the namespace where the User class is referenced as well as change where the password is being set to no longer be encrypted (as the mutator will do this already).

$factory->define(App\Models\User::class, function (Faker $faker) {
return [
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'password' => 'secret',
'remember_token' => str_random(10),
];
});

I like to use the password secret for testing but feel free to update the string literal with any password you like.

Next, update where the User class is referenced in config/auth.php with the model’s new namespace.

'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
    // 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],

Finally, add an auto-incrementing id column to the password_resets table using the increments method. Update the up method in database/migrations/*_create_password_resets_table.php to match below. Notes the * in your file name will be replaced with a date and time.

/**
* Run the migrations.
*
*
@return void
*/
public function up()
{
Schema::create('password_resets', function (Blueprint $table) {
$table->increments('id');
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}

Authentication

We’re ready to generate scaffolding for Laravel’s authentication system! We can do this with a convenient artisan command.

$ php artisan make:auth

This will do five things for us:

  1. Modify our routes/web.php file adding the statementAuth::routes();
  2. Create a HomeController at app/Http/Controllers/HomeController.php
  3. Create an application layout (blade template) at resources/views/layouts/app.blade.php
  4. Create a home view (blade template) at resources/views/home.blade.php
  5. Create authentication views (blade templates) for logging in, resetting passwords, and registering in resources/views/auth/

Updates to Scaffolding

There are a few places we need to update so the scaffolding works with our project. Open up the RegisterController at app/Http/Controllers/RegisterController.php.

At the top of the file, update the use statement to reflect the namespace of our User model.

use App\Models\User;

Next update the docblock on the create method. Also update where the password is set to not encrypt the password, as our mutator on the User model will handle this for us.

/**
* Create a new user instance after a valid registration.
*
*
@param array $data
*
@return \App\Models\User
*/
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => $data['password'],
]);
}

Note we no longer need the use Illuminate\Support\Facades\Hash; at top of the file.

Moving on, open up the ResetPasswordController at app/Http/Controllers/ResetPasswordController.php and notice the controller uses the Illuminate\Foundation\Auth\ResetsPasswords trait. If you look at the resetPassword method on this trait you’ll see we would be encrypting our password twice because the mutator on our User model will also encrypt the password. To prevent this double encryption let’s override the resetPassword method in our ResetPassword controller.

/**
* Reset the given user's password.
*
*
@param \Illuminate\Contracts\Auth\CanResetPassword $user
*
@param string $password
*
@return void
*/
protected function resetPassword($user, $password)
{
$user->password = $password;
    $user->setRememberToken(str_random(60));
    $user->save();
    event(new PasswordReset($user));
    $this->guard()->login($user);
}

Be sure to add use Illuminate\Auth\Events\PasswordReset; at the top of the file.

Test Users

To create some test users for our application let’s create a seeder called UsersTableSeeder using an artisan command.

$ php artisan make:seeder UsersTableSeeder

Open the generated seeder at database/seeds/UsersTableSeeder.php and edit the contents to match below.

<?php
use App\Models\User;
use Illuminate\Database\Seeder;
class UsersTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
*
@return void
*/
public function run()
{
// create a test user
factory(User::class)->create([
'name' => 'Test User',
'email' => 'user@example.com'
]);

// create 50 random test users
factory(User::class, 50)->create();
}
}

Notice how we’re using factories to create our test users. The first statement in the run method creates a test user and modifies the name and email attributes to something we know. The second statement creates 50 random test users using the default UserFactory.

Next open database/seeds/DatabaseSeeder.php and modify the run method to run our new seeder. Notice how the call method is accepting an array of seeder classes. This will make it easy in the future to simply add another seeder to the array.

<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
*
@return void
*/
public function run()
{
$this->call([
UsersTableSeeder::class,
]);
}
}

Finally, let’s migrate and seed our database using the migrate artisan command in combination with the --seed flag.

$ php artisan migrate --seed

Testing Authentication

Before we get to more coding, let’s quickly test the functionality of our application to verify our changes.

Refresh task-app.test in your browser to see a new menu in the top right.

This appeared because of the conditional statement in resources/views/welcome.blade.

@if (Route::has('login'))
<div class="top-right links">
@auth
<a href="{{ url('/home') }}">Home</a>
@else
<a href="{{ route('login') }}">Login</a>
<a href="{{ route('register') }}">Register</a>
@endauth
</div>
@endif

If the route definition has a route named login, render the div contained by the directive. Inside the directive is another directive @auth which acts just like the directive @if (Auth::check()). If the user is authenticated, display a link for the home page. Otherwise, display links for logging in and registering.

Click on “LOGIN”.

Fill in the credentials for our test user and click on “Login”. Remember that unless you changed it to something else, the password we set in our UserFactory is secret.

You will be redirected to the home page for our test user.

Using the menu in the top right, log out of the application.

Next let’s test user registration. Click on “REGISTER” in the menu in the top right.

Register for a new user account using any name, email address, and password.

You will be logged in and redirected to the home page for your new user.

Again, log out of the application using the menu in the top right.

Now let’s test the forgot password functionality of our application.

Click on “LOGIN” in the top right menu.

This time, click on the link “Forgot Your Password?”.

Enter the email address for our test user user@example.com and click “Send Password Reset Link”.

You will receive a confirmation message on the next page.

Now go to your mailtrap account where you will be able to see the email that would have been sent. To learn about customizing this email, read the Laravel documentation on mail.

Note how the domain used in the password reset link is the domain we configured for the APP_URL environment variable. Laravel uses this environment variable when generating URLs for the application.

Click the “Reset Password” button in the email and you will be directed to a reset password form in our application at the task-app.test domain.

Enter user@exmaple.com for the email address and enter and confirm a new password.

Click on “Reset Password” and you’ll receive a confirmation message on the next page.

Just like that, we have a full authentication system complete with logging in and out, user registration, and password resets! I encourage you to read more about Laravel authentication in the documentation.


Implementing Tasks

With our authentication system in place it’s time to implement the functionality of a task list for each user account.

Model, Migration, and Test Data

First let’s generate a Task model and database migration using an artisan command. Note how we can specify the namespace of our new model by escaping backslashes. This namespace will be prefixed with App.

$ php artisan make:model Models\\Task --migration

Open up the generated migration at database/migrations/*_create_tasks_table.php and edit the contents to match below.

We’ll be adding a user_id foreign key to the users table, a string named title for each task, a boolean flag is_complete for tracking if each task is complete, and timestamps for tracking when each task is created and updated, respectively. The timestamps are added using the timestamps method, which creates two nullable timestamps named created_at and updated_at.

<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTasksTable extends Migration
{
/**
* Run the migrations.
*
*
@return void
*/
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id');
$table->string('title');
$table->boolean('is_complete');
$table->timestamps();
            $table
->foreign('user_id')
->references('id')
->on('users');
});
}
    /**
* Reverse the migrations.
*
*
@return void
*/
public function down()
{
Schema::dropIfExists('tasks');
}
}

Next update our Task model by editing the contents of app/Models/Task.php.

<?php

namespace
App\Models;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
/**
* The attributes that should be cast to native types.
*
*
@var array
*/
protected $casts = [
'is_complete' => 'boolean',
];

/**
* The attributes that are mass assignable.
*
*
@var array
*/
protected $fillable = [
'title',
'is_complete',
];

/**
* The relationship to the owning user.
*
*
@return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

The $casts array tells Laravel to automatically cast the is_complete attribute to a boolean type.

The $fillable array tells Laravel we want to be able to “fill” the title and is_complete attributes with mass assignment arrays using methods such as create, fill, or the model’s constructor.

The user method on the model defines a “belongs to” relationship to the User model. This is the user that owns the task.

Let’s add the inverse relationship to our User model by opening up app/Models/Users.php and adding a tasks method which defines a “has many” relationship to the Task model.

/**
* The relationship to the user's tasks.
*
*
@return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function tasks()
{
return $this->hasMany(Task::class);
}

In order to generate some test data for our application let’s define a TaskFactory. Create a file at database/factories/TaskFactory.php and edit the contents to match below.

<?php
use Faker\Generator as Faker;
$factory->define(App\Models\Task::class, function (Faker $faker) {
return [
'title' => $faker->sentence,
'is_complete' => $faker->boolean,
];
});

With this definition we can use the factory method to generate random instances of our Task model, using Faker for random sentences and boolean values.

Next generate a TasksTableSeeder using the artisan command make:seeder.

$ php artisan make:seeder TasksTableSeeder

Edit the generated file at database/seeds/TasksTableSeeder.php and modify its contents to create 10–20 tasks for each user.

<?php

use
App\Models\Task;
use App\Models\User;
use Illuminate\Database\Seeder;

class TasksTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
*
@return void
*/
public function run()
{
// get all users
$users = User::all();

// loop through each user
foreach ($users as $user) {
// determine how many tasks to create for the user
$limit = random_int(10, 20);

// create a new task until the limit is hit
for ($i = 0; $i < $limit; $i++) {
// make a new random task
$task = factory(Task::class)->make();

// associate the task to the user
$task->user()->associate($user);

// save the task
$task->save();
}
}
}
}

To seed our tasks table we first get all users in the database. Next we loop through each user and determine a random number of tasks (between 10 and 20) to create for the user. On each iteration of the for loop until the limit is reached we use the factory helper method to make a new instance of a Task, associate the task with the user, and save the task.

Now let’s migrate our database using the migrate artisan command to create our tasks table.

$ php artisan migrate

Let’s also run our TaskTableSeeder using the db:seed artisan command with the --class flag.

$ php artisan db:seed --class TasksTableSeeder

As a sanity check let’s fire up Tinker and retrieve the tasks associated with the first user in the database.

$ php artisan tinker
>>> App\Models\User::first()->tasks

Tinker will display the collection of Task models associated with the first user. Exit Tinker by typing exit.

Finally, edit database/seeds/DatabaseSeeder.php and add our TasksTableSeeder to the array passed to the call method. This way any time our database is seeded using the db:seed or migrate --seed commands our new seeder will be included.

<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
*
@return void
*/
public function run()
{
$this->call([
UsersTableSeeder::class,
TasksTableSeeder::class,
]);
}
}

Routes, Controller, and Policy

Next let’s get to work on our controllers. Delete the file at app/Http/Controllers/HomeController.php as we don’t need it anymore.

Edit app/Http/Controllers/Auth/LoginController.php and update the $redirectTo property to redirect to the route /tasks.

/**
* Where to redirect users after login.
*
*
@var string
*/
protected $redirectTo = '/tasks';

Make the same change in both app/Http/Controllers/Auth/RegisterController.php and app/Http/Controller/Auth/ResetPasswordController.php.

Let’s define the resource routes in routes/web.php as well as remove the entry for the home controller. Edit routes/web.php to match the contents below.

<?php

Route::get('/', function () {
return view('welcome');
});

Auth::routes();

Route::middleware(['auth'])->group(function() {
Route::resource('tasks', 'TaskController', [
'only' => [
'index', 'store', 'update'
]
]);

});

Notice how we’re using the auth middleware for our route group. This will ensure only authenticated users can access the routes in the group. Inside the group we’re defining three routes to be handled by a TaskController:

  • GET /tasks (index)
  • POST /tasks (store)
  • PATCH /tasks/{task} (update)

Now that we’ve defined the /tasks route, let’s quickly edit the resources/views/welcome.blade.php file to link to our new route instead of /home.

Change the line <a href="{{ url('/home') }}">Home</a> to the following:

<a href="{{ url('/tasks') }}">Tasks</a>

Next let’s generate a TaskController using the artisan command make:controller.

$ php artisan make:controller TaskController

Open up the generated file at app/Http/Controllers/TaskController.php and add the methods index, store, and update.

<?php

namespace
App\Http\Controllers;

use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class TaskController extends Controller
{
/**
* Paginate the authenticated user's tasks.
*
*
@return \Illuminate\View\View
*/
public function index()
{
// paginate the authorized user's tasks with 5 per page
$tasks = Auth::user()
->tasks()
->orderBy('is_complete')
->orderByDesc('created_at')
->paginate(5);

// return task index view with paginated tasks
return view('tasks', [
'tasks' => $tasks
]);
}

/**
* Store a new incomplete task for the authenticated user.
*
*
@param \Illuminate\Http\Request $request
*
@return \Illuminate\Http\RedirectResponse
*/
public function store(Request $request)
{
// validate the given request
$this->validate($request, [
'title' => 'required|string|max:255',
]);

// create a new incomplete task with the given title
Auth::user()->tasks()->create([
'title' => $request->input('title'),
'is_complete' => false,
]);

// flash a success message to the session
session()->flash('status', 'Task Created!');

// redirect to tasks index
return redirect('/tasks');
}

/**
* Mark the given task as complete and redirect to tasks index.
*
*
@param \App\Models\Task $task
*
@return \Illuminate\Routing\Redirector
*
@throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Task $task)
{
// check if the authenticated user can complete the task
$this->authorize('complete', $task);

// mark the task as complete and save it
$task->is_complete = true;
$task->save();

// flash a success message to the session
session()->flash('status', 'Task Completed!');

// redirect to tasks index
return redirect('/tasks');
}
}

The index method paginates the authenticated user’s tasks, 5 per page, and returns the tasks view with the$task paginator as data for the view.

The store method validates the given request, creates a new incomplete task associated with the authenticated user, flashes a success message to the session, and redirects the user to the tasks index. Data flashed to the session is only stored for the next request.

The update method asserts the authenticated user is authorized to complete the given task, marks the task as complete, flashes a success message to the session, and redirects the user to the tasks index.

In order for the authorize method to check if the authenticated user is authorized to complete the task, we need to create a TaskPolicy.

Once again, we can do this with a convenient artisan command.

$ php artisan make:policy TaskPolicy

Edit the created file at app/Policies/TaskPolicy.php to match below.

<?php
namespace App\Policies;
use App\Models\Task;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class TaskPolicy
{
use HandlesAuthorization;
    /**
* Determine if a user can complete a task.
*
*
@param \App\Models\User $user
*
@param \App\Models\Task $task
*
@return bool
*/
public function complete(User $user, Task $task)
{
return $user->is($task->user);
}
}

Here the complete method name matches the string literal passed to the authorize method in the update action method of our Task controller. The first parameter $user will be the authenticated user. The second parameter $task is the task the authenticated user is trying to modify. We return a boolean indicating if the $user is the same user that owns the $task.

In order for Laravel to associate our policy with our Task model we need to add an entry to the $policies array in app/Providers/AuthServiceProvider.php. Open that file and edit the contents to match below.

<?php
namespace App\Providers;
use App\Models\Task;
use App\Policies\TaskPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
*
@var array
*/
protected $policies = [
Task::class => TaskPolicy::class,
];
    /**
* Register any authentication / authorization services.
*
*
@return void
*/
public function boot()
{
$this->registerPolicies();
}
}

Task List View

To create a task list view, rename resources/views/home.blade.php to resources/views/tasks.blade.php then open up the renamed file and edit the contents.

@extends('layouts.app')

@section(
'content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif

<div class="card card-new-task">
<div class="card-header">New Task</div>

<div class="card-body">
<form method="POST" action="{{ route('tasks.store') }}">
@csrf
<div class="form-group">
<label for="title">Title</label>
<input id="title" name="title" type="text" maxlength="255" class="form-control{{ $errors->has('title') ? ' is-invalid' : '' }}" autocomplete="off" />
@if ($errors->has('title'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('title') }}</strong>
</span>
@endif
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</div>
</div>
<div class="card">
<div class="card-header">Tasks</div>

<div class="card-body">
<table class="table table-striped">
@foreach ($tasks as $task)
<tr>
<td>
@if ($task->is_complete)
<s>{{ $task->title }}</s>
@else
{{ $task->title }}
@endif
</td>
<td class="text-right">
@if (! $task->is_complete)
<form method="POST" action="{{ route('tasks.update', $task->id) }}">
@csrf
@method(
'PATCH')
<button type="submit" class="btn btn-primary">Complete</button>
</form>
@endif
</td>
</tr>
@endforeach
</table>

{{ $tasks->links() }}
</div>
</div>
</div>
</div>
</div>
@endsection

At the top, @extends('layouts.app') tells the blade templating engine we’re extending the layout at resources/views/layouts/app.blade.php. Next we’re using the @section directive with the argument 'content' to define the content of our template which will be rendered in the content section of our app layout.

Any messages flashed to the session under the key of status will be shown to the user because of the conditional statement @if (session('status'). Inside this statement is a div with Bootstrap classes for an alert which displays the contents of session('status').

In the <div class="card card-new-task"> tag we are defining a form for the user to use to create new tasks. The form’s action {{ route('tasks.store') }} uses Laravel’s route helper method to generate a URL for our form to POST to. The @csrf blade directive generates a Cross-Site Request Forgery token. The conditional blade directive @if ($errors->has('title')) checks if the errors message bag has an entry for title. If it does it will render the HTML inside the directive, in our case showing the error message with {{ $errors->first('title') }}.

Next in the <div class="card"> tag we have our table of tasks. We loop through each task in the $tasks paginator with the blade directive @foreach ($tasks as $task) and write a table row for each one. In the first <td> tag we conditionally show the task name with or without the <s> tag in order to toggle strikethrough depending on if the task has been completed. In the second <td> tag we’re conditionally showing a form only when the task is incomplete. This form again uses the route() helper method to generate a URL for our form to POST to only this time we’re specifying $task->id as a second argument so the helper method fills the route definition’s $task parameter with the task’s ID. In addition to the @csrf directive we used before, we’re using the @method directive to spoof the form method to be a PATCH instead of a POST.

Finally, the {{ $tasks->links() }} statement generates pagination links for display to our user.

There’s one CSS class we used in our view, .card-new-task that hasn’t been defined yet. Let’s do that now by editing resources/views/layouts/app.blade.php and adding a <style> tag to the bottom of the <head> tag.

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">

<title>{{ config('app.name', 'Laravel') }}</title>

<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>

<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Raleway:300,400,600" rel="stylesheet" type="text/css">

<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
<style>
.card-new-task {
margin-bottom: 20px;
}
</style>
</head>

Using Our Application

Refresh the task-app.test page in your browser to see the results of our work.

There’s now a New Task card at the top of the page.

Followed by a Tasks card with a paginated table of tasks.

Incomplete tasks are listed first. Later in the pagination are completed tasks.

Let’s test our application by creating a new task.

You’ll see the confirmation message we’re flashing to the session after clicking “Create”. Our new task appears at the top of our task list.

Click “Complete” next to our newly created task. The task will be marked as complete and be shown at the beginning of the completed tasks.

Go ahead and log out of the application using the menu in the upper right corner. Log in as the other user you registered and see there are no tasks associated with the user account. Each user has their own private list of tasks!

As you create new tasks, they will appear in your task list.

Conclusion

That’s it! With very little effort, thanks to the Laravel framework, we’ve implemented a user-based task list application complete with user accounts, private task lists, and password resets.

We learned about authentication, route middleware and groups, resource routes, artisan commands, database migrations, eloquent models, relationships, mutators, policies, factories and seeders, blade directives, pagination, and flashing to the session.

You can view the source code for this project on GitHub.

Thanks for reading! See you next time!

Like what you read? Give Brice Hartmann a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.