Sistem informasi perpustakaan adalah proyek klasik yang wajib dikuasai, tetapi banyak pemula menyederhanakannya terlalu jauh — misalnya hanya membuat CRUD buku tanpa mengurus ketersediaan, jatuh tempo, dan denda. Padahal justru di sanalah logika bisnis yang sesungguhnya. Tutorial ini membangun perpustakaan yang benar di Laravel 11 dengan tiga entitas inti dan alur pinjam-kembali yang aman.
1. Merancang Entitas
Kita butuh tiga tabel: books (data buku beserta jumlah kopi), members (anggota), dan loans (transaksi peminjaman). Kunci desain: setiap judul buku bisa punya beberapa eksemplar, sehingga ketersediaan dihitung dari stok kopi dikurangi peminjaman yang belum dikembalikan.
php artisan make:model Book -m
php artisan make:model Member -m
php artisan make:model Loan -m
Migration books dan members
Schema::create('books', function (Blueprint $table) {
$table->id();
$table->string('isbn')->unique()->nullable();
$table->string('title');
$table->string('author');
$table->unsignedInteger('copies')->default(1); // total eksemplar
$table->timestamps();
});
Schema::create('members', function (Blueprint $table) {
$table->id();
$table->string('code')->unique(); // nomor anggota
$table->string('name');
$table->string('phone')->nullable();
$table->timestamps();
});
Migration loans
Tabel peminjaman menyimpan tanggal pinjam, jatuh tempo, dan tanggal kembali. Selama returned_at masih null, buku dianggap sedang dipinjam.
Schema::create('loans', function (Blueprint $table) {
$table->id();
$table->foreignId('book_id')->constrained();
$table->foreignId('member_id')->constrained();
$table->date('borrowed_at');
$table->date('due_at');
$table->date('returned_at')->nullable();
$table->unsignedInteger('fine')->default(0);
$table->timestamps();
});
php artisan migrate
2. Model dan Cek Ketersediaan
Pada model Book, kita hitung berapa eksemplar yang sedang dipinjam, lalu ketersediaan = total kopi dikurangi yang dipinjam. Ini mencegah buku dipinjam melebihi jumlah eksemplar.
class Book extends Model
{
protected $fillable = ['isbn', 'title', 'author', 'copies'];
public function loans()
{
return $this->hasMany(Loan::class);
}
public function getOnLoanAttribute(): int
{
return $this->loans()->whereNull('returned_at')->count();
}
public function getAvailableAttribute(): int
{
return max(0, $this->copies - $this->on_loan);
}
}
Model Loan menyimpan konstanta aturan main — lama pinjam dan tarif denda per hari — agar mudah diubah di satu tempat:
class Loan extends Model
{
const LOAN_DAYS = 7; // durasi pinjam standar
const FINE_PER_DAY = 1000; // denda per hari terlambat
protected $fillable = ['book_id', 'member_id', 'borrowed_at', 'due_at', 'returned_at', 'fine'];
protected $casts = [
'borrowed_at' => 'date',
'due_at' => 'date',
'returned_at' => 'date',
];
}
3. Alur Peminjaman
Saat anggota meminjam, kita cek ketersediaan dulu, lalu buat transaksi dengan jatuh tempo otomatis. Gunakan DB::transaction dan lockForUpdate supaya dua anggota tidak berebut eksemplar terakhir secara bersamaan.
use App\Models\Loan;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
public function borrow(Request $request)
{
$data = $request->validate([
'book_id' => 'required|exists:books,id',
'member_id' => 'required|exists:members,id',
]);
DB::transaction(function () use ($data) {
$book = Book::lockForUpdate()->findOrFail($data['book_id']);
if ($book->available < 1) {
abort(422, 'Semua eksemplar buku ini sedang dipinjam.');
}
Loan::create([
'book_id' => $book->id,
'member_id' => $data['member_id'],
'borrowed_at' => now(),
'due_at' => now()->addDays(Loan::LOAN_DAYS),
]);
});
return back()->with('success', 'Peminjaman berhasil dicatat.');
}
4. Alur Pengembalian dan Hitung Denda
Saat buku dikembalikan, kita bandingkan tanggal kembali dengan jatuh tempo. Jika terlambat, denda = jumlah hari keterlambatan dikalikan tarif harian. Perhatikan penggunaan diffInDays dengan urutan argumen yang benar agar keterlambatan positif.
public function returnBook(Loan $loan)
{
if ($loan->returned_at) {
return back()->with('error', 'Buku ini sudah dikembalikan.');
}
$today = Carbon::today();
$fine = 0;
if ($today->greaterThan($loan->due_at)) {
$lateDays = $loan->due_at->diffInDays($today);
$fine = $lateDays * Loan::FINE_PER_DAY;
}
$loan->update([
'returned_at' => $today,
'fine' => $fine,
]);
return back()->with('success', $fine > 0
? "Buku dikembalikan. Denda: Rp " . number_format($fine)
: 'Buku dikembalikan tepat waktu.');
}
5. Daftar Buku Terlambat
Petugas perlu melihat siapa yang masih menahan buku melewati jatuh tempo. Query-nya cukup memfilter peminjaman yang belum kembali dan sudah lewat tanggal:
$overdue = Loan::query()
->whereNull('returned_at')
->whereDate('due_at', '<', now())
->with(['book', 'member'])
->get();
foreach ($overdue as $loan) {
$lateDays = $loan->due_at->diffInDays(now());
// tampilkan $loan->member->name, $loan->book->title, $lateDays
}
Kesalahan Umum yang Perlu Dihindari
- Menyimpan status "dipinjam" sebagai boolean di tabel buku: gagal menangani banyak eksemplar. Hitung ketersediaan dari kopi dikurangi peminjaman aktif.
- Tidak memakai transaksi saat meminjam: eksemplar terakhir bisa dipinjam dua orang sekaligus.
- Salah urutan diffInDays: pastikan menghitung selisih dari jatuh tempo ke hari ini agar denda tidak negatif.
Dengan struktur tiga entitas dan aturan yang tersimpan sebagai konstanta, sistem perpustakaan Anda mudah dikembangkan: tambahkan perpanjangan pinjam, batas maksimal buku per anggota, atau notifikasi jatuh tempo lewat email tanpa merombak model data.