Skip to content

Build a library app with Laravel 10 and MySQL

Learn how to build a Laravel application backed by a MySQL PlanetScale database.

Build a library app with Laravel 10 and MySQL

Laravel is arguably the most popular PHP framework available today. It provides tooling that enables developers to implement complex application features, such as authentication and rate limiting, with single commands. It also integrates seamlessly with MySQL-powered databases — a popular and reliable database management system.

In this tutorial, I will show you how to build a library app using Laravel and MySQL with PlanetScale. PlanetScale is a cloud-native, MySQL compatible database platform. It makes it easy to scale MySQL horizontally while providing features like high availability through automatic replication, and database branching with zero-downtime schema migrations.

The app will have two features: authentication and loan management. The loan management feature enables logged-in users to borrow and return as many books as possible. This feature also implements checks to prevent the "over-borrowing" of books.

Get started

To follow this tutorial, you will need the following:

Set up the PlanetScale database

In your PlanetScale dashboard, click on New database, specify a database name (for example, library-app), and click Create database to complete the process. You will be redirected to the database overview.

While the provisioning process of the database is ongoing, you can optionally configure your database to automatically copy migration data to new branches as they are created . Go to the Settings section and select the option to Automatically copy migration data. Make sure you specify Laravel for the migration framework.

With this feature enabled, the migration metadata will also be transferred to the production branch when changes are merged in. You can read more about it in our docs.

Back in the Dashboard section, click the Connect button in the top right. This will take you to the Connect page, where you will be able to create a new password. After creating a password, choose Laravel in the Select your language or framework section and copy the credentials shown in the .env file in the next section. Be careful with this information as it is only shown once. If you lose the credentials, you will have to recreate them.

With your database in place, it’s time to build your application.

Scaffold the application

Create a new application using the following command:

Terminal
composer create-project laravel/laravel library-app

When the project setup is complete, open the library-app project in your preferred IDE or text editor.

To connect your application to your PlanetScale database, update the following .env variables to match the ones you copied earlier. Make sure to leave the other values as they are.

Terminal
DB_CONNECTION=mysql
DB_HOST=<YOUR_DB_HOST>
DB_PORT=3306
DB_DATABASE=<YOUR_DB_DATABASE>
DB_USERNAME=<YOUR_DB_USERNAME>
DB_PASSWORD=<YOUR_DB_PASSWORD>
MYSQL_ATTR_SSL_CA=/etc/ssl/cert.pem

Run the following command to check for a successful connection to the database, create database tables, and publish its schema.

Terminal
php artisan migrate

Navigate to your PlanetScale dashboard and click Tables to view the database tables for your application.

Add authentication

Laravel Breeze will be used to implement authentication. Install it with the following command.

Terminal
composer require laravel/breeze --dev

Laravel Breeze makes provisioning for various “stack” flavours ranging from React/Vue to Inertia SSR. We will use the default, which is the Blade stack. Install Laravel Breeze as shown below.

Terminal
php artisan breeze:install

You will be prompted to respond to few questions. Choose blade as the stack and response with no to using dark mode support and Pest.

Next, issue the following command to update the database and install frontend dependencies;

Terminal
php artisan migrate
npm install

With just two commands, you have written and published the authentication views, routes, controllers, and other resources to your application.

Add the models

In addition to the User model created automatically by Laravel Breeze, we’ll need two other models: Book and Loan.

Create the Book model using the following command:

Terminal
php artisan make:model Book -cfms

The c argument tells Artisan to create a controller in addition to the model. In the same way, f is for factory, m is for migration, and s is for seeder. This will allow you to set your application up with pre-saved books.

Next, create the Loan model using the following command.

Terminal
php artisan make:model Loan -mc

For the Loan model, only a controller and migration are required alongside the Book model.

Update the migrations

Next, update the migrations for the Book and Loan models. Laravel migrations are found in the database/migrations folder. The migration name is prepended with a timestamp in the YYYY_MM_DD_HHMMSS format. To keep things simple, the timestamp will be skipped when migration filenames are discussed.

Start by updating the migration for the Book model. Open database/migrations/create_books_table.php and update the up function to match the following.

PHP
Schema::create('books', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('author');
$table->string('year');
$table->integer('copies_in_circulation');
$table->timestamps();
});

This adds three string columns: title, author, year (the year the book was released), and an integer column named copies_in_circulation which indicates how many copies are in the library’s possession.

Next, open database/migrations/create_loans_table.php and update the up function as shown below.

PHP
Schema::create('loans', function (Blueprint $table) {
$table->id();
$table->foreignId('book_id');
$table->foreignId('user_id');
$table->integer('number_borrowed');
$table->dateTime('return_date');
$table->boolean('is_returned')->default(false);
$table->timestamps();
});

Update the factory

The next step is to update the Book factory. Open database/factories/BookFactory.php and update the definition function to match the following.

PHP
return [
'title' => fake()->sentence(),
'author' => fake()->name,
'year' => fake()->year(),
'copies_in_circulation' => fake()->numberBetween(1, 20)
];

Update the seeder

First, add the import statement for the Book model if your IDE did not already do so automatically.

PHP
use App\Models\Book;

Next, update the seeder for the Book model. Open database/seeders/BookSeeder.php and add the following to the run function.

PHP
Book::factory()->times(20)->create();

Also, within the database/seeders/DatabaseSeeder.php, Add the import statement for the User model if it’s not present.

PHP
use App\Models\User;

Next, open database/seeders/DatabaseSeeder.php and update the run function as shown below.

PHP
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
$this->call([BookSeeder::class]);

Run the new migrations and seed your database using the following command:

Terminal
php artisan migrate --seed

Your PlanetScale database will be populated with 20 new books as shown below.

Fillable properties, relationships, helper functions

Next, update your models to include fillable properties, relationships, and helper functions. This will make for better separation of concerns and slimmer controller functions.

Start with the User model created by Laravel Breeze. Open app/Models/User.php and add this import statement:

PHP
use Illuminate\Database\Eloquent\Relations\HasMany;

Then, update the content by adding the following functions to it.

PHP
public function activeLoans() {
return $this
->loans()
->where('is_returned', false)
->get();
}
public function loans(): HasMany {
return $this->hasMany(Loan::class);
}

The loans function is used to specify the relationship between the User model and the Loan model as a user can have more than one loan.

The activeLoans function will be used in the controller (or blade template) to access the books which the user has borrowed.

Next, update the Book model. Open app/Models/Book.php and update the code to match the following.

PHP
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Book extends Model {
use HasFactory;
public function canBeBorrowed(): bool {
return $this->activeLoans() < $this->copies_in_circulation;
}
private function activeLoans(): int {
return $this->loans()
->where('is_returned', false)
->get()
->sum('number_borrowed');
}
public function loans(): HasMany {
return $this->hasMany(Loan::class);
}
public function availableCopies(): int {
return $this->copies_in_circulation - $this->activeLoans();
}
}

The canBeBorrowed function returns a boolean to indicate whether or not there are still copies available for users to borrow.

The loans function is used to specify the relationship between the Book and Loan models. Depending on the number of books in the library, it is possible for more than one loan to be taken out for a book.

The activeLoans function uses the loans relationship to retrieve all the loans associated with a book and get only the ones which have is_returned set to false.

The last function, availableCopies, returns the number of copies still available to be borrowed.

Next, update the Loan model by changing the code in app/Models/Loan.php to match the following:

PHP
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Loan extends Model {
use HasFactory;
protected $fillable = [
'number_borrowed',
'return_date',
'book_id',
'user_id',
];
public function user(): BelongsTo {
return $this->belongsTo(User::class);
}
public function book(): BelongsTo {
return $this->belongsTo(Book::class);
}
public function terminate() {
$this->is_returned = true;
$this->save();
}
}

Here, the fillable property is specified. In addition to that, the inverse relation between the Loan and User models and Loan and Book models are also included.

The terminate function is also added, which will be used to terminate a loan when the user returns the associated book.

Finalizing the controllers

Next, add functions to handle incoming requests in your controllers. Start with the BookController. Open app/Http/Controllers/BookController.php and update the code to match the following.

PHP
<?php
namespace App\Http\Controllers;
use App\Models\Book;
use Illuminate\Contracts\View\View;
class BookController extends Controller {
public function index(): View {
return view('books.index', ['books' => Book::all()]);
}
}

The controller has only one function (index), which returns all the books in the library.

Next, open app/Http/Controllers/LoanController.php and update the code to match the following.

PHP
<?php
namespace App\Http\Controllers;
use App\Models\Book;
use App\Models\Loan;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator as ValidatorFacade;
use Illuminate\Validation\Validator;
use Illuminate\View\View;
class LoanController extends Controller {
public function index(): View {
return view('loans.index', ['loans' => Auth::user()->activeLoans()]);
}
public function store(Book $book, Request $request): RedirectResponse {
$validator = ValidatorFacade::make($request->all(), [
'number_borrowed' => 'required|int',
'return_date' => 'required',
]);
$validator->after(function (Validator $validator) use ($book) {
$numberBorrowed = $validator->safe()->number_borrowed;
$availableCopies = $book->availableCopies();
if ($numberBorrowed > $availableCopies) {
$validator->errors()->add(
'number_borrowed',
"You cannot borrow more than {$availableCopies} book(s)"
);
}
});
if ($validator->fails()) {
return to_route('loans.create', ['book' => $book])
->withErrors($validator)
->withInput();
}
$loanDetails = $validator->safe()->only([
'number_borrowed',
'return_date',
]);
$loanDetails['book_id'] = $book->id;
$loanDetails['user_id'] = Auth::user()->id;
Loan::create($loanDetails);
return to_route('loans.index')
->with('status', 'Book borrowed successfully');
}
public function create(Book $book): View {
return view('loans.create', ['book' => $book]);
}
public function terminate(Loan $loan): RedirectResponse {
$loan->terminate();
return to_route('loans.index')
->with('status', 'Book returned successfully');
}
}

The index function returns all the loans for the logged-in user using the Auth facade and the activeLoans function declared earlier.

The store function takes a Book (obtained via Route Model Binding) and a request and creates a new Loan for the logged-in user. The provided request is validated to ensure that the number of books to be borrowed and the return date are specified. It then runs another check to make sure that the number of books to be borrowed is not more than the copies available. This is because while a certain number of books may be in circulation, some loans may already have been created for some of the copies in circulation. If all checks pass, a new loan is created and the user is redirected to a page showing all the active loans. Otherwise, the user is redirected to the loan form where the errors will be rendered accordingly.

The create function takes a Book (also obtained via Route Model Binding) and returns a form which the user must fill and submit to complete the borrowing process.

Finally, the terminate function takes a Loan and calls the terminate function on that loan which effectively makes it available for future loans. The user is also redirected to the list of active loans.

With the controllers in place, the next thing to do is create and update the application views (blade templates).

Update views

In this section, you will modify the default navigation bar included by Laravel breeze and create views for listing, borrowing and viewing books that you have borrowed.

Update navigation

In addition to adding authentication to the application, Laravel Breeze also added a navigation bar that, with some minor adjustments, can be used in your application. Open resources/views/layouts/navigation.blade.php and replace the content with the following code.

PHP
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}">
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
</a>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
</div>
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ml-6">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
<div>{{ Auth::user()->name }}</div>
<div class="ml-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('books.index')">
{{ __('Books') }}
</x-dropdown-link>
<x-dropdown-link :href="route('loans.index')">
{{ __('Loans') }}
</x-dropdown-link>
<x-dropdown-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
<!-- Hamburger -->
<div class="-mr-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="px-4">
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
</div>
<div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-responsive-nav-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-responsive-nav-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
</div>
</div>
</div>
</nav>

Next, create a new folder named books within the resources/views folder and then create a new file within it. You can call the file index.blade.php. Open the newly created file and update its content as shown here:

PHP
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet"/>
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen">
@if (Route::has('login'))
<div class="p-6 text-right">
@auth
@include('layouts.navigation')
@else
<a href="{{ route('login') }}"
class="font-semibold text-gray-600 hover:text-gray-900">Log
in</a>
@if (Route::has('register'))
<a href="{{ route('register') }}"
class="ml-4 font-semibold text-gray-600 hover:text-gray-900">Register</a>
@endif
@endauth
</div>
@endif
<h2 class="m-6 text-xl font-semibold text-gray-900 text-center">Laravel Library App</h2>
<table class="mx-auto">
<thead>
<tr>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
#
</th>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
Title
</th>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
Author
</th>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
Release Year
</th>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
Released Copies
</th>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
Available Copies
</th>
@auth
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
Actions
</th>
@endif
</tr>
</thead>
<tbody class="bg-white">
@foreach($books as $book)
<tr>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200">{{ $loop->index + 1 }}</td>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200">
<div class="flex items-center">
<div class="text-sm font-medium leading-5 text-gray-900">
{{$book->title}}
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200">
<div class="text-sm leading-5 text-gray-500">{{$book->author}}</div>
</td>
<td
class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-no-wrap border-b border-gray-200">
{{$book->year}}
</td>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200">
@if($book->copies_in_circulation < 10)
<span
class="inline-flex px-2 text-xs font-semibold leading-5 text-red-800 bg-red-100 rounded-full">
{{$book->copies_in_circulation}}
</span>
@elseif($book->copies_in_circulation < 20)
<span
class="inline-flex px-2 text-xs font-semibold leading-5 text-orange-800 bg-orange-100 rounded-full">
{{$book->copies_in_circulation}}
</span>
@else
<span
class="inline-flex px-2 text-xs font-semibold leading-5 text-green-800 bg-green-100 rounded-full">
{{$book->copies_in_circulation}}
</span>
@endif
</td>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200">
@if($book->availableCopies() < 10)
<span
class="inline-flex px-2 text-xs font-semibold leading-5 text-red-800 bg-red-100 rounded-full">
{{$book->availableCopies()}}
</span>
@elseif($book->availableCopies() < 20)
<span
class="inline-flex px-2 text-xs font-semibold leading-5 text-orange-800 bg-orange-100 rounded-full">
{{$book->availableCopies()}}
</span>
@else
<span
class="inline-flex px-2 text-xs font-semibold leading-5 text-green-800 bg-green-100 rounded-full">
{{$book->availableCopies()}}
</span>
@endif
</td>
@auth
<td
class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-no-wrap border-b border-gray-200">
@if($book->canBeBorrowed())
<a href="{{ route('loans.create', ['book' => $book->id]) }}">Borrow book</a>
@else
<p class="text-red-600"> No copies available to borrow</p>
@endif
</td>
@endif
</tr>
@endforeach
</tbody>
</table>
</div>
</body>
</html>

This view will be used for listing books.

Now, create another folder named loans within resources/views. Next, create a new file index.blade.php within the new folder and use the following content for it:

PHP
@php use Carbon\Carbon; @endphp
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Active Loans </h2>
</x-slot>
<div class="py-12">
<div class="shadow-sm sm:rounded-lg">
<div class="mx-auto sm:px-6 lg:px-8">
@if (session()->has('status'))
<div
class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative w-7/12 text-center mx-auto m-5"
role="alert">
<span class="block sm:inline">{{ session()->get('status') }}</span>
</div>
@endif
@if($loans->count() > 0)
<table class="mx-auto">
<thead>
<tr>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
#
</th>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
Book
</th>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
Number Borrowed
</th>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
Return date
</th>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white">
@foreach($loans as $loan)
<tr>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200">{{ $loop->index + 1 }}</td>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200">
<div class="flex items-center">
<div class="text-sm font-medium leading-5 text-gray-900">
{{$loan->book->title}}
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200">
<div class="text-sm leading-5 text-gray-500">{{$loan->number_borrowed}}</div>
</td>
<td
class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-no-wrap border-b border-gray-200">
{{Carbon::parse($loan->return_date)->format('l jS F, Y')}}
</td>
<td
class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-no-wrap border-b border-gray-200">
<a href="{{ route('loans.terminate', ['loan' => $loan->id]) }}">Return book</a>
</td>
</tr>
@endforeach
</tbody>
</table>
@else
<h1 class="m-6 text-xl font-semibold text-gray-900 text-center">You have no active loans</h1>
@endif
</div>
</div>
</div>
</x-app-layout>

Finally, create a new file named create.blade.php in the resources/views/loans folder and add the following code to it.

PHP
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Borrow Book') }} "{{$book->title}}"
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<form method="POST" action="{{ route('loans.store', ['book' => $book->id]) }}">
@csrf
<div class="mb-6">
<label class="block">
<span class="text-gray-700">How many copies would you like to borrow?</span>
<input type="number" name="number_borrowed" class="block w-full mt-1 rounded-md"
placeholder="How many copies would you like to borrow?"
value="{{old('number_borrowed')}}"/>
</label>
@error('number_borrowed')
<div class="text-sm text-red-600">{{ $message }}</div>
@enderror
</div>
<div class="mb-6">
<label class="block">
<span class="text-gray-700">Return date</span>
<input type="date" name="return_date" class="block w-full mt-1 rounded-md"
placeholder="" value="{{old('return_date')}}"/>
</label>
@error('return_date')
<div class="text-sm text-red-600">{{ $message }}</div>
@enderror
</div>
<x-primary-button type="submit">
Submit
</x-primary-button>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>

Update the routes

At this point, you have all the pieces of the puzzle in place. All that’s left is to update your route declaration so that requests are handled appropriately by the links provided in the views or when a controller redirects the user to a different part of the application.

Update routes/web.php to match the following.

PHP
<?php
use App\Http\Controllers\BookController;
use App\Http\Controllers\LoanController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
Route::get('/', [BookController::class, 'index']);
Route::get('/dashboard', [BookController::class, 'index'])
->middleware(['auth', 'verified'])
->name('dashboard');
Route::middleware('auth')->group(function () {
Route::get('/books', [BookController::class, 'index'])->name('books.index');
Route::get('/loans', [LoanController::class, 'index'])->name('loans.index');
Route::get('/loans/{book}', [LoanController::class, 'create'])->name('loans.create');
Route::post('/loans/{book}', [LoanController::class, 'store'])->name('loans.store');
Route::get('/loans/terminate/{loan}', [LoanController::class, 'terminate'])->name('loans.terminate');
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
require __DIR__.'/auth.php';

And that’s it! To run your application, open a terminal and run:

Terminal
npm run dev

Open another terminal window and in this one, run:

Terminal
php artisan serve

By default, the application is served on port 8000. Navigate to http://127.0.0.1:8000 to launch the application. To log in, you can either register or use the pre-saved user (email address: test@example.com, password: password). When you are logged in, you will be able to borrow books by clicking the Borrow book link and then filling out the form.

Conclusion

In this article, we have discussed how to build a PHP application using Laravel, Laravel Breeze for authentication, Eloquent as an ORM, Blade for templating, and a MySQL database hosted on PlanetScale. You started by creating a new database on PlanetScale, and then set up a Laravel project and connected it to the database. Using PHP Artisan and Eloquent, you set up your models and controllers. Finally, you updated the views to render the content returned by the controllers.

By following the steps outlined in this article, you should now have a functional Laravel 10 application that interacts with a MySQL database hosted on PlanetScale. You should also have a good understanding of how to build a web application using Laravel and MySQL. With this knowledge, you can continue to build more complex applications or customize this application to meet your specific needs. The entire codebase is available on GitHub. Happy Coding!

Want a powerful and performant database that doesn’t slow you down?