Kritim Yantra
Apr 19, 2025
Introduction
Building a Library Management System is a classic project that lets you apply core Laravel concepts—from routing and MVC structure to database design, authentication, and more. In this guide, we’ll walk through creating a full-featured Library Management System using Laravel 12, covering everything from initial setup to deployment. Whether you’re a beginner or looking to sharpen your Laravel skills, you’ll find clear, step‑by‑step instructions and best practices throughout.
User Roles
Book Management
Member Management
Loan Processing
Search & Filters
Notifications
Reporting
Before you begin, ensure you have:
Create a new Laravel app
composer create-project laravel/laravel library-system "12.*"
cd library-system
Serve locally
php artisan serve
# Visit http://127.0.0.1:8000
Install front‑end dependencies
npm install
npm run dev
id, title, isbn, author_id, category_id, total_copies, available_copies, ...
id, name, bio
id, name
id, user_id, membership_date, status
id, book_id, member_id, issued_at, due_at, returned_at, fine_amount
php artisan make:migration create_authors_table
php artisan make:migration create_categories_table
php artisan make:migration create_books_table
php artisan make:migration create_members_table
php artisan make:migration create_loans_table
Edit each migration under database/migrations/
:
2025_04_19_000000_create_authors_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('authors', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('bio')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('authors');
}
};
2025_04_19_000001_create_categories_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('categories');
}
};
2025_04_19_000002_create_books_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('books', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('isbn')->unique();
$table->foreignId('author_id')->constrained()->onDelete('cascade');
$table->foreignId('category_id')->constrained()->onDelete('cascade');
$table->unsignedInteger('total_copies');
$table->unsignedInteger('available_copies');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('books');
}
};
2025_04_19_000003_create_members_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('members', function (Blueprint $table) {
$table->id();
// Assumes you have a users table already for authentication
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->date('membership_date')->default(DB::raw('CURRENT_DATE'));
$table->string('status')->default('pending'); // e.g. pending, active, suspended
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('members');
}
};
2025_04_19_000004_create_loans_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('loans', function (Blueprint $table) {
$table->id();
$table->foreignId('book_id')->constrained()->onDelete('cascade');
$table->foreignId('member_id')->constrained()->onDelete('cascade');
$table->timestamp('issued_at')->useCurrent();
$table->timestamp('due_at');
$table->timestamp('returned_at')->nullable();
$table->decimal('fine_amount', 8, 2)->default(0);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('loans');
}
};
php artisan migrate
Create models with their relationships:
php artisan make:model Author -m
php artisan make:model Category -m
php artisan make:model Book -m
php artisan make:model Member -m
php artisan make:model Loan -m
Define relationships:
// app/Models/Author.php
public function books()
{
return $this->hasMany(Book::class);
}
// app/Models/Category.php
public function books()
{
return $this->hasMany(Book::class);
}
// app/Models/Book.php
public function author()
{
return $this->belongsTo(Author::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
public function loans()
{
return $this->hasMany(Loan::class);
}
// app/Models/Member.php
public function user()
{
return $this->belongsTo(User::class);
}
public function loans()
{
return $this->hasMany(Loan::class);
}
// app/Models/Loan.php
public function book()
{
return $this->belongsTo(Book::class);
}
public function member()
{
return $this->belongsTo(Member::class);
}
Use Laravel Breeze or Jetstream for authentication:
composer require laravel/breeze --dev
php artisan breeze:install
npm install && npm run dev
php artisan migrate
Add a role
field to users
table via a migration:
php artisan make:migration add_role_to_users_table
// in migration
$table->string('role')->default('member');
Seed an admin user:
User::factory()->create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => bcrypt('secret'),
'role' => 'admin',
]);
Protect routes by middleware:
Generate the middleware using Artisan:
php artisan make:middleware CheckRole
This creates the file app/Http/Middleware/CheckRole.php
, where you can implement your role‑checking logic.
In app/Http/Middleware/CheckRole.php
, add your authorization logic. For example:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckRole
{
public function handle(Request $request, Closure $next, string $role)
{
if (! $request->user() || $request->user()->role !== $role) {
abort(403, 'Unauthorized.');
}
return $next($request);
}
}
This middleware checks the authenticated user’s role
property against the required role parameter.
bootstrap/app.php
Starting in Laravel 12, you no longer register middleware aliases in the HTTP kernel. Instead, use the withMiddleware()
callback in bootstrap/app.php
:
<?php
use App\Http\Middleware\CheckRole;
use Illuminate\Foundation\Application;
use Illuminate\Routing\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
console: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
// Define a short alias 'role' for the CheckRole middleware
$middleware->alias([
'role' => CheckRole::class,
]);
})
->withExceptions(function ($exceptions) {
// Exception handling...
})
->create();
withMiddleware()
method accepts a closure that receives a Middleware
registrar instance. $middleware->alias()
to map the 'role'
key to your middleware class. $middleware->alias([
'role' => CheckRole::class,
'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class,
]);
With the alias registered, you can attach it to individual routes or entire route groups without referencing the class name:
// routes/web.php
// Protect a single route with the 'role:admin' middleware
Route::get('/admin/dashboard', [AdminController::class, 'index'])
->middleware('role:admin');
// Apply to a group of routes
Route::middleware(['auth', 'role:librarian'])->group(function () {
Route::get('/librarian/books', [BookController::class, 'index']);
// ... other librarian routes
});
'role:admin'
syntax passes admin
as the $role
parameter to your CheckRole
middleware. auth
and verified
Laravel 12 also lets you reorder middleware execution or modify predefined groups in bootstrap/app.php
:
->withMiddleware(function (Middleware $middleware) {
// Ensure CheckRole runs after Authenticate
$middleware->priority([
\Illuminate\Auth\Middleware\Authenticate::class,
CheckRole::class,
]);
// Add CheckRole to the 'web' group
$middleware->web(append: [
CheckRole::class,
]);
})
$middleware->priority()
to specify execution order. $middleware->web()
or $middleware->api()
to append or prepend middleware to the default groups.This ensures your role
alias correctly guards routes based on the user’s role.
Generate resource controllers:
php artisan make:controller Admin/AuthorController --resource --model=Author
php artisan make:controller Admin/CategoryController --resource --model=Category
php artisan make:controller Admin/BookController --resource --model=Book
php artisan make:controller Member/LoanController --resource --model=Loan
Define routes in routes/web.php
:
Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->group(function () {
Route::resource('authors', AuthorController::class);
Route::resource('categories', CategoryController::class);
Route::resource('books', BookController::class);
});
Route::middleware(['auth', 'role:member'])->group(function () {
Route::resource('loans', LoanController::class);
});
Implement CRUD methods (index, create, store, show, edit, update, destroy) in each controller, using standard validation:
// In BookController@store
$request->validate([
'title' => 'required|string|max:255',
'isbn' => 'required|string|unique:books',
'author_id' => 'required|exists:authors,id',
'category_id' => 'required|exists:categories,id',
'total_copies' => 'required|integer|min:1',
]);
Book::create([
...$request->only(['title','isbn','author_id','category_id']),
'available_copies' => $request->total_copies,
]);
In LoanController
:
public function store(Request $request)
{
$request->validate([
'book_id' => 'required|exists:books,id',
]);
$book = Book::findOrFail($request->book_id);
if ($book->available_copies < 1) {
return back()->withErrors('No copies available');
}
$loan = Loan::create([
'book_id' => $book->id,
'member_id' => auth()->user()->member->id,
'issued_at' => now(),
'due_at' => now()->addWeeks(2),
]);
$book->decrement('available_copies');
return redirect()->route('loans.index')->with('success', 'Book issued successfully.');
}
public function update(Request $request, Loan $loan)
{
$loan->update([
'returned_at' => now(),
'fine_amount' => $loan->due_at->isPast()
? now()->diffInDays($loan->due_at) * 5
: 0,
]);
$loan->book->increment('available_copies');
return back()->with('success', 'Book returned.');
}
resources/views/layouts/app.blade.php
. authors/index.blade.php
, create.blade.php
, edit.blade.php
. Use Bootstrap or TailwindCSS (built into Breeze) for styling:
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
Add search form in BookController@index
:
public function index(Request $request)
{
$query = Book::with(['author','category']);
if ($search = $request->input('q')) {
$query->where('title', 'like', "%{$search}%")
->orWhere('isbn', 'like', "%{$search}%");
}
$books = $query->paginate(10);
return view('admin.books.index', compact('books'));
}
In Blade:
<form method="GET" action="{{ route('admin.books.index') }}">
<input type="text" name="q" value="{{ request('q') }}" placeholder="Search books…">
<button type="submit">Search</button>
</form>
.env
. php artisan make:mail DueReminderMail
php artisan make:command SendDueReminders
In the command’s handle()
:Loan::whereDate('due_at', now()->addDay())
->whereNull('returned_at')
->get()
->each(function($loan) {
Mail::to($loan->member->user->email)
->send(new DueReminderMail($loan));
});
app/Console/Kernel.php
: $schedule->command('reminders:due')->dailyAt('08:00');
public function test_admin_can_create_book()
{
$admin = User::factory()->create(['role' => 'admin']);
$this->actingAs($admin)
->post(route('admin.books.store'), [
'title' => 'Test Book',
'isbn' => '1234567890',
'author_id' => Author::factory()->create()->id,
'category_id' => Category::factory()->create()->id,
'total_copies' => 5,
])
->assertRedirect(route('admin.books.index'));
$this->assertDatabaseHas('books', ['title' => 'Test Book']);
}
Run tests:
php artisan test
php artisan config:cache
php artisan route:cache
php artisan view:cache
public/
directory). supervisor
for background jobs (email reminders). You’ve built a robust Library Management System in Laravel 12, covering:
To deepen your knowledge:
Keep iterating on the project—add new features, refactor code, and follow the latest Laravel releases. Happy coding!
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google