In this tutorial, we'll be creating a login and registration system with one of the most popular PHP frameworks known as Laravel. In short, Laravel is the foundation for modern web apps that want to utilize server-side programming, database management, and more.

With the MVC design pattern that Laravel implements, we're able to create a clean and seamless system that separates our app into three components (Model, View, and Controller). It enables us to organize and structure our app much better while leveraging the core methods that Laravel has to offer.

The login system will include MySQL database interaction (to store user accounts), HTML forms to create and authorize users, and a homepage for authorized users.

1. Getting Started

Before we dive into creating the login system, let's go ahead and configure the necessary components.

1.1. Requirements

The following are required:

  • XAMPP — while you can download PHP & MySQL separately, XAMPP includes all the essentials for deploying web apps on a development environment, which makes it convenient to use.
  • Composer — dependency manager for PHP that's required to install Laravel.
  • Code Editor — Visual Studio Code is recommended.
  • Laravel 11.x — The PHP framework we'll be using in this tutorial.
  • PHP 8 or higher — not required if using XAMPP.

Once you've finished installing XAMPP, follow the below instructions:

  • Open the XAMPP Control Panel.
  • Next to the Apache module select Start.
  • Next to the MySQL module select Start.
  • If successful, both modules will be highlighted in green.

We can now proceed to the next step.

1.2. Database Schema (SQL)

The MySQL database will enable us to create and retrieve user accounts, but before we get to that point, we must create the database along with the accounts table.

Assuming you're using XAMPP to manage your databases, you can proceed to follow the below instructions. If not, you'll need to install a database management tool like MySQL Workbench.

  • Navigate to http://localhost/phpmyadmin in your web browser.
  • In the top navigation bar, select the SQL tab.
  • Execute the following SQL statement:
SQL
CREATE DATABASE IF NOT EXISTS `laravel_login_system` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `laravel_login_system`;

CREATE TABLE IF NOT EXISTS `accounts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `password` varchar(255) NOT NULL,
  `email` varchar(100) NOT NULL,
  `registered` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

That will create the laravel_login_system database schema that we can use to store captured form data, including username, email, and password. The ID column is unique and will be used to identify the user.

You could also generate a database migration with Laravel and declare your tables and columns directly with code. For this tutorial, however, we'll go with the much easier approach.

Tip Consider using Laravel's migrations instead of raw SQL for database schema management. Migrations allow you to version control your database changes and make collaboration and deployment easier.

1.3. Install & Configure Laravel

Open the command line and execute the following command:

cd C:\xampp\htdocs

And then create our Laravel project with:

composer create-project --prefer-dist laravel/laravel laravel-login-system

Now, let's configure the .env file — navigate to your project directory and edit the .env file with your code editor. Update the DB_* variables to reflect the following:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_login_system
DB_USERNAME=root
DB_PASSWORD=

Update the values to reflect your database credentials. If you're using XAMPP, you shouldn't have to change them.

Next, find the SESSION_DRIVER variable and update it to:

SESSION_DRIVER=file

And let's temporarily disable caching:

CACHE_DRIVER=array

This will help speed up the development phase and avoid executing unnecessary commands.

2. Stylesheet (CSS)

The stylesheet will cover the design aspect of our app and format it accordingly.

Navigate to the public/css directory, create a new file called app.css, and add the following CSS code:

CSS
* {
  box-sizing: border-box;
  font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
  font-size: 16px;
}
body, html {
  background-color: #f7f4f3;
  margin: 0;
  padding: 0;
}
h1, h2, h3, h4, h5, h6 {
  margin: 0;
  padding: 0;
  color: #504947;
}
.form {
  display: flex;
  flex-flow: column;
  width: 100%;
}
.form .form-label {
  display: block;
  padding: 20px 0 10px 0;
  font-weight: 500;
  font-size: 14px;
  color: #504947;
}
.form .form-group {
  display: flex;
  position: relative;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}
.form .form-group .form-icon-left, .form .form-group .form-icon-right {
  fill: #cbc4c1;
  width: 40px;
  position: absolute;
  transform: translateY(-50%);
  top: 50%;
  pointer-events: none;
}
.form .form-group .form-icon-left {
  left: 0;
}
.form .form-group .form-icon-left + .form-input {
  padding-left: 40px;
}
.form .form-group .form-icon-right {
  right: 0;
}
.form .form-group .form-icon-right + .form-input {
  padding-right: 40px;
}
.form .form-group:focus-within .form-icon-left {
  fill: #a89d98;
}
.form .form-input {
  width: 100%;
  height: 43px;
  border: 1px solid #e6e0de;
  padding: 0 15px;
  border-radius: 4px;
  color: #000;
}
.form .form-input::placeholder {
  color: #a89d98;
}
.form .form-link {
  color: #eb512a;
  font-weight: 500;
  text-decoration: none;
  font-size: 14px;
}
.form .form-link:hover {
  color: #c53713;
}
.form p.register-link {
  margin: 0;
  padding: 20px 0 0 0;
  font-size: 14px;
  color: #796e6b;
}
.form div.msg.error {
  padding: 0 0 20px 0;
  color: #c21e13;
}
.form div.msg.error .form-link {
  color: #74160b;
}
.form div.msg.error .form-link:hover {
  color: #450d07;
}
.form div.msg p {
  margin: 0;
  padding: 0;
  font-size: 14px;
  font-weight: 500;
  color: inherit;
}
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  appearance: none;
  cursor: pointer;
  border: 0;
  background: #d6523e;
  color: #FFFFFF;
  padding: 0 14px;
  font-size: 14px;
  font-weight: 600;
  border-radius: 4px;
  height: 42px;
  box-shadow: 0px 0px 6px 1px rgba(68, 52, 45, 0.1);
}
.btn:hover {
  background: #d34731;
}
.login, .register {
  display: flex;
  flex-flow: column;
  width: 400px;
  max-width: 95%;
  background-color: #ffffff;
  box-shadow: 0px 0px 7px 1px rgba(68, 52, 45, 0.05);
  border-radius: 5px;
  margin: 100px auto;
  padding: 35px;
}
.login h1, .register h1 {
  text-align: center;
  font-size: 24px;
  font-weight: 500;
  padding: 15px 0;
  margin: 0;
}
.header {
  background-color: #413533;
  height: 60px;
  width: 100%;
}
.header .wrapper {
  display: flex;
  justify-content: space-between;
  align-items: center;
  position: relative;
  margin: 0 auto;
  width: 900px;
  height: 100%;
}
.header .wrapper h1, .header .wrapper a {
  display: inline-flex;
  align-items: center;
}
.header .wrapper h1 {
  font-size: 20px;
  padding: 0;
  margin: 0;
  color: #fff;
  font-weight: normal;
}
.header .wrapper .menu {
  display: flex;
  align-items: center;
}
.header .wrapper .menu a {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 32px;
  padding: 0 12px;
  margin: 0 3px;
  text-decoration: none;
  color: #e2dedd;
  font-weight: 500;
  font-size: 16px;
  line-height: 16px;
}
.header .wrapper .menu a svg {
  fill: #e2dedd;
  margin: 2px 8px 0 0;
}
.header .wrapper .menu a:hover, .header .wrapper .menu a:active {
  color: #ecebeb;
}
.header .wrapper .menu a:hover svg, .header .wrapper .menu a:active svg {
  fill: #ecebeb;
}
.header .wrapper .menu a:last-child {
  margin-right: 0;
}
.content {
  width: 900px;
  margin: 0 auto;
}
.content .page-title {
  display: flex;
  align-items: center;
  padding: 25px 0 10px 0;
}
.content .page-title h2 {
  margin: 0;
  padding: 0 0 7px 0;
  font-size: 20px;
  font-weight: 600;
  color: #5e5553;
}
.content .page-title p {
  margin: 0;
  padding: 0;
  font-size: 14px;
  color: #867a77;
}
.content .block {
  box-shadow: 0px 0px 7px 1px rgba(68, 52, 45, 0.05);
  margin: 25px 0;
  padding: 25px;
  border-radius: 5px;
  background-color: #fff;
}
.content .block p {
  padding: 7px;
  margin: 0;
}
.content .profile-detail {
  display: flex;
  flex-flow: column;
  font-size: 18px;
  padding: 5px 0;
}
.content .profile-detail strong {
  display: block;
  color: #9e9592;
  font-size: 14px;
  font-weight: 500;
  margin-bottom: 2px;
}
.pad-1 {
  padding: 5px;
}
.mar-1 {
  margin: 5px;
}
.pad-2 {
  padding: 10px;
}
.mar-2 {
  margin: 10px;
}
.pad-3 {
  padding: 15px;
}
.mar-3 {
  margin: 15px;
}
.pad-4 {
  padding: 20px;
}
.mar-4 {
  margin: 20px;
}
.pad-5 {
  padding: 25px;
}
.mar-5 {
  margin: 25px;
}
.pad-bot-1 {
  padding-bottom: 5px;
}
.pad-top-1 {
  padding-top: 5px;
}
.pad-left-1 {
  padding-left: 5px;
}
.pad-right-1 {
  padding-right: 5px;
}
.pad-x-1 {
  padding-left: 5px;
  padding-right: 5px;
}
.pad-y-1 {
  padding-top: 5px;
  padding-bottom: 5px;
}
.mar-bot-1 {
  margin-bottom: 5px;
}
.mar-top-1 {
  margin-top: 5px;
}
.mar-left-1 {
  margin-left: 5px;
}
.mar-right-1 {
  margin-right: 5px;
}
.mar-x-1 {
  margin-left: 5px;
  margin-right: 5px;
}
.mar-y-1 {
  margin-top: 5px;
  margin-bottom: 5px;
}
.pad-bot-2 {
  padding-bottom: 10px;
}
.pad-top-2 {
  padding-top: 10px;
}
.pad-left-2 {
  padding-left: 10px;
}
.pad-right-2 {
  padding-right: 10px;
}
.pad-x-2 {
  padding-left: 10px;
  padding-right: 10px;
}
.pad-y-2 {
  padding-top: 10px;
  padding-bottom: 10px;
}
.mar-bot-2 {
  margin-bottom: 10px;
}
.mar-top-2 {
  margin-top: 10px;
}
.mar-left-2 {
  margin-left: 10px;
}
.mar-right-2 {
  margin-right: 10px;
}
.mar-x-2 {
  margin-left: 10px;
  margin-right: 10px;
}
.mar-y-2 {
  margin-top: 10px;
  margin-bottom: 10px;
}
.pad-bot-3 {
  padding-bottom: 15px;
}
.pad-top-3 {
  padding-top: 15px;
}
.pad-left-3 {
  padding-left: 15px;
}
.pad-right-3 {
  padding-right: 15px;
}
.pad-x-3 {
  padding-left: 15px;
  padding-right: 15px;
}
.pad-y-3 {
  padding-top: 15px;
  padding-bottom: 15px;
}
.mar-bot-3 {
  margin-bottom: 15px;
}
.mar-top-3 {
  margin-top: 15px;
}
.mar-left-3 {
  margin-left: 15px;
}
.mar-right-3 {
  margin-right: 15px;
}
.mar-x-3 {
  margin-left: 15px;
  margin-right: 15px;
}
.mar-y-3 {
  margin-top: 15px;
  margin-bottom: 15px;
}
.pad-bot-4 {
  padding-bottom: 20px;
}
.pad-top-4 {
  padding-top: 20px;
}
.pad-left-4 {
  padding-left: 20px;
}
.pad-right-4 {
  padding-right: 20px;
}
.pad-x-4 {
  padding-left: 20px;
  padding-right: 20px;
}
.pad-y-4 {
  padding-top: 20px;
  padding-bottom: 20px;
}
.mar-bot-4 {
  margin-bottom: 20px;
}
.mar-top-4 {
  margin-top: 20px;
}
.mar-left-4 {
  margin-left: 20px;
}
.mar-right-4 {
  margin-right: 20px;
}
.mar-x-4 {
  margin-left: 20px;
  margin-right: 20px;
}
.mar-y-4 {
  margin-top: 20px;
  margin-bottom: 20px;
}
.pad-bot-5 {
  padding-bottom: 25px;
}
.pad-top-5 {
  padding-top: 25px;
}
.pad-left-5 {
  padding-left: 25px;
}
.pad-right-5 {
  padding-right: 25px;
}
.pad-x-5 {
  padding-left: 25px;
  padding-right: 25px;
}
.pad-y-5 {
  padding-top: 25px;
  padding-bottom: 25px;
}
.mar-bot-5 {
  margin-bottom: 25px;
}
.mar-top-5 {
  margin-top: 25px;
}
.mar-left-5 {
  margin-left: 25px;
}
.mar-right-5 {
  margin-right: 25px;
}
.mar-x-5 {
  margin-left: 25px;
  margin-right: 25px;
}
.mar-y-5 {
  margin-top: 25px;
  margin-bottom: 25px;
}

We could use Bootstrap to design our app, but to keep it minimal and compact, we'll include our customized stylesheet. In addition, it demonstrates how you can incorporate your own stylesheets as opposed to using composer to install libraries such as Bootstrap.

3. Create the Account Model

The account model interacts directly with our accounts table in our database. It will be responsible for creating and managing our accounts.

Leveraging the Auth classes will enable us to create accounts, authenticate the user, and retrieve data associated with the user.

Execute the following command to create a new model:

php artisan make:model Account

Once created, we can proceed to edit the file and update it to reflect the following:

PHP
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class Account extends Authenticatable
{
    // Update the created_at column name to match the database column
    const CREATED_AT = 'registered';
    // Disable the updated_at column as it doesn't exist in the database
    const UPDATED_AT = null;

    // The table name
    protected $table = 'accounts';

    // The fields that are required for registration
    protected $fillable = [
        'username', 'email', 'password',
    ];

    // The fields that are hidden from the user (password and remember_token)
    protected $hidden = [
        'password', 'remember_token',
    ];

    // Use 'username' for authentication
    public function getAuthIdentifierName()
    {
        return 'username';
    }
}
?>

The fillable fields will be username, email, and password, and will be required during the registration process.

The getAuthIdentifierName() method will use the username field to authenticate the user. This is important because we're using the username to authenticate the user, not the email.

There are a bunch of methods that we will utilize that aren't declared in the model because they're already bundled with Laravel, so we don't need to create methods that will create the account, etc.

4. Configure Authentication to Use the Account Model

Configuring the app/auth.php file will enable us to use our own account model instead of using Laravel's default classes to handle user authentication.

Edit the app/auth.php file and update it to reflect the following:

PHP
<?php

return [

    // Update the providers array to use the Account model
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\Account::class,
        ],
    ],

    // Update the guards array
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
    ],

    'defaults' => [
        'guard' => env('AUTH_GUARD', 'web'),
        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
    ],

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),

];

?>

The providers array will use the Account model to handle user authentication. The guards array will use the web driver to authenticate users.

5. Create Authenticaton Controllers

In this section, we'll create the authentication controllers that will handle HTTP requests for our login, register, home, and profile pages.

The controller handles incoming and outgoing HTTP requests with methods like POST, GET, and PUT. With the request object, we can validate data and return a response.

5.1. Login Controller

Let's begin by creating the login controller. Execute the following artisan command:

php artisan make:controller Auth/LoginController

Now, let's edit our newly created login controller and update it to reflect the following:

PHP
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    // Show login form
    public function showLoginForm()
    {
        return view('layouts.login');
    }

    // Handle login
    public function login(Request $request)
    {
        // Validate the request data
        $credentials = $request->validate([
            'username' => 'required|string|max:50',
            'password' => 'required|string',
        ]);

        // Attempt to log in using the provided credentials and the Auth facade (class)
        if (Auth::attempt($credentials)) {
            // Regenerate the session to prevent session fixation
            $request->session()->regenerate();

            // Redirect to intended page or home
            return redirect()->intended('home');
        }

        // Invalid credentials, output error message
        return back()->withErrors([
            'username' => 'Invalid credentials.',
        ])->withInput();
    }

    // Handle logout
    public function logout(Request $request)
    {
        Auth::logout();

        // Invalidate the session
        $request->session()->invalidate();
        $request->session()->regenerateToken();

        return redirect('login');
    }
}

?>

The validate method validates input data based on the desired pattern and returns false if the captured form data doesn't meet the requirements. Once validated, the code will attempt to authenticate the user with the attempt method.

The session regenerate method will regenerate the session ID and will help mitigate session fixation attacks.

Tip Utilize Laravel's built-in validation rules in your controllers to safeguard against invalid or malicious input. This ensures that only properly formatted data is processed and stored.

If the login attempt is successful, we can use the redirect method to redirect the user to the homepage, which will be restricted to authenticated users.

The logout method will destroy the active session and redirect the user back to the login page.

5.2. Register Controller

The registration controller will be responsible for capturing form data and inserting a new record in our accounts table.

Let's create the register controller with the following artisan command:

php artisan make:controller Auth/RegisterController

Once created, update the controller to reflect the following:

PHP
<?php

namespace App\Http\Controllers;

use App\Models\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;

class RegisterController extends Controller
{
    // Show registration form
    public function showRegistrationForm()
    {
        return view('layouts.register');
    }

    // Handle registration
    public function register(Request $request)
    {
        // Validate the request data
        $data = $request->validate([
            'username' => 'required|string|max:50|unique:accounts',
            'email' => 'required|string|email|max:100|unique:accounts',
            'password' => 'required|string|min:8',
        ]);

        // Create a new account
        $account = Account::create([
            'username' => $data['username'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);

        // Authenticate the new account
        Auth::login($account);

        // Redirect to homepage
        return redirect('home');
    }
}

?>

Like we did before, we're utilizing methods that are included with Laravel, such as login, validate, and redirect. These methods are essential for the registration process.

Let's break down the validation rules so we can get a better understanding:

  • required — will ensure the field is required, meaning it cannot be empty or null.
  • string — the field must be of data type string.
  • email — the field must be an email.
  • max:50 — the maximum number of characters the field can contain (50 characters).
  • min:8 — the minimum number of characters the field can contain (8 characters).
  • unique — the field must be unique and cannot exist in the accounts table.

The Hash::make method will generate a secure hash for the password field. It will prevent the account passwords from being exposed and enhance the security of the system.

5.3. Home Controller

The home controller will handle the homepage and will be restricted to authenticated users.

Let's create the home controller with the following artisan command:

php artisan make:controller Auth/HomeController

Edit the home controller and update it to reflect the following:

PHP
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;

class HomeController extends Controller
{
    public function index()
    {
        // Retrieve the authenticated account details
        $account = Auth::user();
        // Return the home view with the account details
        return view('layouts.home', ['account' => $account]);
    }
}

?>

The Auth::user method will retrieve account details associated with the user. It will be useful when we want to output account data in our template files, such as the username.

That's essentially all we need to implement for the home controller. We can prevent unauthorized access to the homepage when we create the home route.

5.4. Profile Controller

Finally, we can finish up the controllers with the profile controller. The profile controller will be responsible for displaying the profile page with the user's details.

To create the profile controller, execute the following artisan command:

php artisan make:controller Auth/ProfileController

After, edit the profile controller and update it to reflect the following:

PHP
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;

class ProfileController extends Controller
{
    public function index()
    {
        // Get the authenticated user details
        $account = Auth::user();

        // Return the profile view with the authenticated user details
        return view('layouts.profile', ['account' => $account]);
    }
}

?>

Now that we have the controllers set up, we can proceed to define the routes and create our template files.

6. Define Routes

We can use routes to map HTTP requests to our controllers. They basically act as an intermediary between the user's request and our app's response. Routes will rewrite the URL to make them appear friendly.

Edit the routes/web.php file and update the file to reflect the following:

PHP
<?php

use Illuminate\Support\Facades\Route;

// Import the controllers
use App\Http\Controllers\LoginController;
use App\Http\Controllers\RegisterController;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\ProfileController;

// Default Route
Route::get('/', [LoginController::class, 'showLoginForm']);

// Registration Routes
Route::get('register', [RegisterController::class, 'showRegistrationForm'])->name('register');
Route::post('register', [RegisterController::class, 'register']);

// Login Routes
Route::get('login', [LoginController::class, 'showLoginForm'])->name('login');
Route::post('login', [LoginController::class, 'login']);

// Logout Route
Route::get('logout', [LoginController::class, 'logout'])->name('logout');

// Home Route
Route::get('home', [HomeController::class, 'index'])->name('home')->middleware('auth');

// Profile Route
Route::get('profile', [ProfileController::class, 'index'])->name('profile')->middleware('auth');

?>

The Route::get method will handle incoming GET requests while the Route::post method will handle incoming POST requests. The POST requests are triggered when you submit a form, etc, while GET requests are triggered by default and don't require any form interaction.

We can prevent unauthorized access to specific pages by attaching the middleware method with the value auth to our routes.

The default route will display the login page.

7. Create Blade Templates

We can use blade templates to structure the HTML and associate them with our controllers. In addition, we can utilize the blade engine to organize our templates.

7.1. Layout Files

The layout templates will be inherited by other templates and will contain the header and footer sections. We can create a master layout file and extend it to other templates.

Navigate to resources/views/layouts, create a new file called master.blade.php, and add the following code:

HTML
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width,minimum-scale=1">
		<title>@yield('title', 'Login System')</title>
		<link href="{{ asset('css/app.css') }}" rel="stylesheet" type="text/css">
	</head>
    <body>

		<header class="header">

			<div class="wrapper">

				<h1>Website Title</h1>
				
				<nav class="menu">
					<a href="{{ route('home') }}">Home</a>
					<a href="{{ route('profile') }}">Profile</a>
					<a href="{{ route('logout') }}">
						<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M377.9 105.9L500.7 228.7c7.2 7.2 11.3 17.1 11.3 27.3s-4.1 20.1-11.3 27.3L377.9 406.1c-6.4 6.4-15 9.9-24 9.9c-18.7 0-33.9-15.2-33.9-33.9l0-62.1-128 0c-17.7 0-32-14.3-32-32l0-64c0-17.7 14.3-32 32-32l128 0 0-62.1c0-18.7 15.2-33.9 33.9-33.9c9 0 17.6 3.6 24 9.9zM160 96L96 96c-17.7 0-32 14.3-32 32l0 256c0 17.7 14.3 32 32 32l64 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-64 0c-53 0-96-43-96-96L0 128C0 75 43 32 96 32l64 0c17.7 0 32 14.3 32 32s-14.3 32-32 32z"/></svg>
						Logout
					</a>
				</nav>

			</div>

		</header>

		<div class="content">
        	@yield('content')
		</div>

    </body>
</html>

This will be used for our authenticated pages, such as home and profile pages.

The @yield directive will display the content of a given section, meaning we can utilize this to display different content on our home and profile pages. It works in conjunction with the @section directive.

The asset and route methods will determine the correct path and URLs to these resources.

Create a new file called auth.blade.php and add the following code:

HTML
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width,minimum-scale=1">
		<title>@yield('title', 'Login System')</title>
		<link href="{{ asset('css/app.css') }}" rel="stylesheet" type="text/css">
	</head>
    <body>
        @yield('content')
    </body>
</html>

The login and registration pages are structured differently, with nothing but a form to input data. Therefore, it's convenient to create a separate layout file for this purpose.

7.2. Login View

The login template will include a form with username and password fields. It will be required to input data and submit it to the server for processing using the HTTP POST method.

Create a new file called login.blade.php file and add the following code:

HTML
@extends('layouts.auth')

@section('title', 'Login')

@section('content')
<div class="login">

    <h1>Member Login</h1>

    <form action="{{ route('login') }}" method="post" class="form login-form">

        @csrf

        <label class="form-label" for="username">Username</label>
        <div class="form-group">
            <svg class="form-icon-left" width="14" height="14" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"/></svg>
            <input class="form-input" type="text" name="username" placeholder="Username" id="username" value="{{ old('username') }}" required>
        </div>

        <label class="form-label" for="password">Password</label>
        <div class="form-group mar-bot-5">
            <svg class="form-icon-left" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M144 144v48H304V144c0-44.2-35.8-80-80-80s-80 35.8-80 80zM80 192V144C80 64.5 144.5 0 224 0s144 64.5 144 144v48h16c35.3 0 64 28.7 64 64V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V256c0-35.3 28.7-64 64-64H80z"/></svg>
            <input class="form-input" type="password" name="password" placeholder="Password" id="password" required>
        </div>

        @if ($errors->any())
        <div class="msg error">
            @foreach ($errors->all() as $error)
                <p>{{ $error }}</p>
            @endforeach
        </div>
        @endif

        <button class="btn blue" type="submit">Login</button>

        <p class="register-link">Don't have an account? <a href="{{ route('register') }}" class="form-link">Register</a></p>

    </form>

</div>
@endsection

As you can see, we're using the @section directive to define the content of the page. The @extends directive will inherit the layout file and display the content within the @section directive.

The @csrf directive will add a token to our form that will prevent Cross-Site Request Forgery attacks. If you're encountering issues with the token, make sure to disable caching for the login and register pages to prevent inconsistent results.

The @if directive will check for errors and populate them with the @foreach directive, which is a loop.

Our login page will resemble the following:

Laravel Login Form with MySQL and PHP

We should now have a fully functioning login form with the ability to input a username and password.

7.3. Registration View

The registration page will be similar to the login page but will include an email field to prevent duplicate accounts with the same email address.

Create a new file called register.blade.php and add the following code:

HTML
@extends('layouts.auth')

@section('title', 'Register')

@section('content')
<div class="register">

    <h1>Member Register</h1>

    <form action="{{ route('register') }}" method="post" class="form register-form">

        @csrf

        <label class="form-label" for="username">Username</label>
        <div class="form-group">
            <svg class="form-icon-left" width="14" height="14" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"/></svg>
            <input class="form-input" type="text" name="username" placeholder="Username" id="username" value="{{ old('username') }}" required>
        </div>

        <label class="form-label" for="email">Email</label>
        <div class="form-group">
            <svg class="form-icon-left" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/></svg>
            <input class="form-input" type="email" name="email" placeholder="Email" id="email" value="{{ old('email') }}" required>
        </div>

        <label class="form-label" for="password">Password</label>
        <div class="form-group mar-bot-5">
            <svg class="form-icon-left" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M144 144v48H304V144c0-44.2-35.8-80-80-80s-80 35.8-80 80zM80 192V144C80 64.5 144.5 0 224 0s144 64.5 144 144v48h16c35.3 0 64 28.7 64 64V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V256c0-35.3 28.7-64 64-64H80z"/></svg>
            <input class="form-input" type="password" name="password" placeholder="Password" id="password" autocomplete="new-password" required>
        </div>
        
        @if ($errors->any())
        <div class="msg error">
            @foreach ($errors->all() as $error)
                <p>{{ $error }}</p>
            @endforeach
        </div>
        @endif

        <button class="btn blue" type="submit">Register</button>

        <p class="register-link">Already have an account? <a href="{{ route('login') }}" class="form-link">Login</a></p>

    </form>

</div>
@endsection

With the registration form, the user can enter an email, username, and password. They can also navigate between pages using the @route directive.

The registration form will resemble the following:

Laravel Registration Form with MySQL and PHP

Next up, we can create our authenticated pages that will be available when the user successfully logs in.

7.4. Home View

We can utilize the home page to let the user know they're logged in to the system. The page will only be available to authenticated users, while the other users will be redirected to the login page.

Create a new file called home.blade.php and add the following code:

HTML
@extends('layouts.master')

@section('title', 'Home')

@section('content')
<div class="page-title">
	<div class="wrap">
		<h2>Home</h2>
		<p>Welcome back, {{{ $account->username }}}!</p>
	</div>
</div>

<div class="block">

	<p>This is the home page. You are logged in!</p>

</div>
@endsection

This will extend the other layout that we're using for authenticated pages. Remember the home controller we created earlier that passes the account details to the view? As we can see here, we are displaying the data directly from the $account object.

The three curly braces securely escape data output and thus mitigate risks of XSS attacks. In the above code, we are escaping the username, meaning all special characters will be converted to HTML entities.

The home page will resemble the following:

Laravel User Home with MySQL and PHP

That's essentially all we need to do for the home template. Feel free to customize the template and include content that you want to show to your authenticated users.

7.5. Profile View

The profile page we can use to display account-related details, such as the username, email, and the date registered.

HTML
@extends('layouts.master')

@section('title', 'Profile')

@section('content')
<div class="page-title">
	<div class="wrap">
		<h2>Profile</h2>
		<p>View your profile details below.</p>
	</div>
</div>

<div class="block">

	<div class="profile-detail">
		<strong>Username</strong>
		{{{ $account->username }}}
	</div>

	<div class="profile-detail">
		<strong>Email</strong>
		{{{ $account->email }}}
	</div>

	<div class="profile-detail">
		<strong>Registered</strong>
		{{{ $account->registered->format('F j, Y') }}}
	</div>

</div>
@endsection

Like before, it's good practice to escape the data using the three curly braces.

The profile page will resemble the following:

Laravel User Profile with MySQL and PHP

With all the views created, we now have a fully functioning system that will allow users to register, log in, and view their profile details.

8. Test the Login System

There are a few approaches we could take to test the system. The first approach would be to navigate to the system directly over localhost using XAMPP. Follow the below instructions.

  • Open the XAMPP control and start Apache and MySQL.
  • Navigate to http://localhost/laravel-login-system/public/ directly in your browser.
  • If you see the login page, the system is working correctly.
  • Register a new account and log in.

The conventional approach would be to use the artisan serve command:

  • Execute the artisan command: php artisan serve
  • Open your browser and navigate to http://localhost:8000.
  • If you see the login page, the system is working correctly.
  • Register a new account and log in.

Once you've successfully logged in, you can navigate to the home and profile pages to view your account details.

Tip If you encounter issues with stale data or configurations, run php artisan cache:clear and php artisan config:clear to clear cached data. This ensures your application uses the most recent code and settings.

Frequently Asked Questions (FAQ)

Below are some common questions and answers that may help you understand the login and registration system better.

What version of Laravel is used in this tutorial?

This tutorial uses Laravel 11. If you're using a different version, some steps or code snippets might be slightly different. Always refer to the official Laravel documentation for guidance.

Do I need to use XAMPP, or can I use another development environment?

You can use any PHP development environment that supports PHP and MySQL. While this tutorial uses XAMPP for simplicity, alternatives like MAMP, WAMP, or Laravel Homestead will also work.

How do I generate an application key for Laravel?

After installing Laravel, open your command line, navigate to your project directory, and run:

php artisan key:generate

This command generates a unique application key, which is essential for securing user sessions and encrypted data.

Can I use migrations instead of raw SQL to create the database schema?

Yes, you can use Laravel's migrations to define your database schema in code. Migrations allow you to version control your database changes and make collaboration easier. To create a migration for the accounts table, run:

php artisan make:migration create_accounts_table

Then, define your table structure in the generated migration file.

I'm getting the error "Target class [LoginController] does not exist." How do I fix it?

This error usually occurs due to incorrect namespace declarations or route definitions. Ensure your controllers are properly namespaced, and update your routes to use fully qualified class names. For example, in your routes/web.php file:

<?php 
use Illuminate\Support\Facades\Route; 
use App\Http\Controllers\Auth\LoginController; 
Route::get('login', [LoginController::class, 'showLoginForm'])->name('login');
?>

Make sure your controller files are in the correct directory and have the appropriate namespace at the top of the file.

Why is my CSS not loading on the login page (404 error)?

A 404 error means the file can't be found. Ensure your CSS file is located in the public/css directory and that you're referencing it correctly in your Blade templates:

<link href="{{ asset('css/app.css') }}" rel="stylesheet" type="text/css">

Also, check that the filename is correct and that there are no typos in the path.

Why am I redirected back to the login page after logging in?

This issue may be due to session misconfiguration or authentication setup. Make sure your SESSION_DRIVER in the .env file is set correctly (e.g., SESSION_DRIVER=file). Also, verify that your authentication configuration in config/auth.php is properly set up to use your Account model.

How do I clear cached configurations in Laravel?

If you're experiencing issues with configuration caching, you can clear it by running:

php artisan config:clear

This ensures that your application uses the latest settings from your .env file.

How can I protect routes so only authenticated users can access them?

You can use middleware to protect your routes. In your routes/web.php file, apply the auth middleware to routes you want to protect:

<?php 
use Illuminate\Support\Facades\Route; 
use App\Http\Controllers\HomeController; 
Route::get('home', [HomeController::class, 'index'])->middleware('auth');
?>

This ensures that only logged-in users can access the home page.

How can I debug errors in my Laravel application?

Check the log files located in storage/logs/laravel.log for detailed error messages. You can also enable debug mode by setting APP_DEBUG=true in your .env file.

Conclusion

That's essentially all we need to do to create a basic login system with Laravel, MySQL, and PHP. We've covered the basics of creating a new Laravel project, setting up the database, creating models, controllers, and views, and defining routes.

With the system in place, you can further customize the system to include additional features, such as password reset, email verification, and more. Laravel provides a wide range of features that can be utilized to enhance the system.

Feel free to customize the system to suit your needs and requirements. If you have any questions or need further assistance, please don't hesitate to reach out to us.

Enjoy coding!