Sistem Informasi Perpustakaan Berbasis Web

Banyak tugas akhir dan proyek sekolah berhenti di CRUD buku: tambah, ubah, hapus, selesai. Begitu dipakai petugas sungguhan, masalah muncul. Satu judul punya tiga eksemplar tapi si...

Sistem Informasi Perpustakaan Berbasis Web

Banyak tugas akhir dan proyek sekolah berhenti di CRUD buku: tambah, ubah, hapus, selesai. Begitu dipakai petugas sungguhan, masalah muncul. Satu judul punya tiga eksemplar tapi sistem hanya mengenal "dipinjam" atau "tersedia", denda dihitung manual, dan tidak ada yang tahu siapa saja yang terlambat mengembalikan buku.

Logika bisnis perpustakaan justru ada di ketersediaan eksemplar, jatuh tempo, denda, dan batas pinjam per anggota. Tutorial ini membangun inti sistem perpustakaan di Laravel 11 (PHP 8.2+) dengan tiga entitas, alur pinjam-kembali yang aman dari rebutan data, serta pengingat otomatis lewat scheduler.

Aturan Bisnis yang Perlu Disepakati Dulu

Sebelum menulis migration, tuliskan aturan main perpustakaan. Nilai di tabel ini adalah contoh; sesuaikan dengan kebijakan perpustakaan Anda.

AturanContoh nilaiDisimpan di
Lama pinjam standar7 hariKonstanta Loan::LOAN_DAYS
Denda per hari terlambatRp1.000Konstanta Loan::FINE_PER_DAY
Maksimal buku aktif per anggota3 bukuKonstanta Loan::MAX_ACTIVE
Perpanjangan1 kali, sebelum jatuh tempoKolom renewals

Menyimpan aturan di satu tempat membuat perubahan kebijakan cukup diubah sekali. Jika aturan sering berubah, pindahkan ke tabel settings agar bisa diatur dari panel admin.

1. Merancang Entitas dan Migration

Kita butuh tiga tabel: books (judul beserta jumlah eksemplar), members (anggota), dan loans (transaksi peminjaman). Kunci desainnya: ketersediaan tidak disimpan sebagai kolom, melainkan dihitung dari jumlah eksemplar dikurangi peminjaman yang belum kembali.

php artisan make:model Book -m
php artisan make:model Member -m
php artisan make:model Loan -m
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();
});

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->unsignedTinyInteger('renewals')->default(0);
    $table->unsignedInteger('fine')->default(0);
    $table->timestamps();

    $table->index(['book_id', 'returned_at']);
    $table->index(['member_id', 'returned_at']);
});

Selama returned_at masih null, peminjaman dianggap aktif. Dua indeks gabungan di akhir mempercepat query yang paling sering dijalankan: "berapa eksemplar buku ini yang sedang dipinjam" dan "berapa buku yang sedang dipinjam anggota ini". Untuk gambaran relasi antar tabel, lihat juga merancang ERD sederhana untuk aplikasi.

2. Model dan Relasi

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Book extends Model
{
    protected $fillable = ['isbn', 'title', 'author', 'copies'];

    public function loans(): HasMany
    {
        return $this->hasMany(Loan::class);
    }

    public function activeLoans(): HasMany
    {
        return $this->loans()->whereNull('returned_at');
    }
}

class Loan extends Model
{
    const LOAN_DAYS    = 7;
    const FINE_PER_DAY = 1000;
    const MAX_ACTIVE   = 3;

    protected $fillable = ['book_id', 'member_id', 'borrowed_at', 'due_at', 'returned_at', 'renewals', 'fine'];

    protected function casts(): array
    {
        return [
            'borrowed_at' => 'date',
            'due_at'      => 'date',
            'returned_at' => 'date',
        ];
    }

    public function book(): BelongsTo   { return $this->belongsTo(Book::class); }
    public function member(): BelongsTo { return $this->belongsTo(Member::class); }
}

Kenapa relasi activeLoans dan bukan accessor yang menjalankan query? Accessor yang memanggil $this->loans()->count() akan menjalankan satu query per buku. Di halaman katalog berisi 50 buku, itu berarti 50 query tambahan. Dengan relasi, kita bisa memakai withCount sehingga semuanya dihitung dalam satu query:

$books = Book::withCount('activeLoans')->orderBy('title')->paginate(20);

// di Blade: {{ $book->copies - $book->active_loans_count }} tersedia

Teknik menghindari N+1 seperti ini dibahas lebih lengkap di optimasi performa query Eloquent Laravel.

3. Alur Peminjaman yang Aman

Saat anggota meminjam, ada tiga pemeriksaan: buku masih punya eksemplar, anggota belum mencapai batas, dan semuanya terjadi atomik. DB::transaction bersama lockForUpdate mencegah dua petugas meminjamkan eksemplar terakhir kepada dua anggota pada detik yang sama.

use App\Models\Book;
use App\Models\Loan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

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->activeLoans()->count() >= $book->copies) {
            abort(422, 'Semua eksemplar buku ini sedang dipinjam.');
        }

        $active = Loan::where('member_id', $data['member_id'])
            ->whereNull('returned_at')
            ->count();

        if ($active >= Loan::MAX_ACTIVE) {
            abort(422, 'Anggota sudah mencapai batas peminjaman.');
        }

        Loan::create([
            'book_id'     => $book->id,
            'member_id'   => $data['member_id'],
            'borrowed_at' => today(),
            'due_at'      => today()->addDays(Loan::LOAN_DAYS),
        ]);
    });

    return back()->with('success', 'Peminjaman berhasil dicatat.');
}

Kunci baris buku membuat permintaan kedua menunggu sampai transaksi pertama selesai, lalu menghitung ulang dengan data terbaru. Tanpa kunci, keduanya bisa membaca "1 eksemplar tersisa" bersamaan.

4. Pengembalian dan Perhitungan Denda

public function returnBook(Loan $loan)
{
    if ($loan->returned_at) {
        return back()->with('error', 'Buku ini sudah dikembalikan.');
    }

    $today = today();
    $lateDays = $today->greaterThan($loan->due_at)
        ? (int) $loan->due_at->diffInDays($today)
        : 0;

    $loan->update([
        'returned_at' => $today,
        'fine'        => $lateDays * Loan::FINE_PER_DAY,
    ]);

    return back()->with('success', $lateDays > 0
        ? 'Buku dikembalikan. Denda: Rp ' . number_format($loan->fine, 0, ',', '.')
        : 'Buku dikembalikan tepat waktu.');
}

Perhatikan dua detail. Pertama, urutan $loan->due_at->diffInDays($today): di Carbon 3 (dipakai Laravel 11), hasil diffInDays bertanda, sehingga urutan terbalik menghasilkan angka negatif. Kedua, Carbon 3 mengembalikan float, jadi kita ubah ke int agar denda tidak berupa pecahan.

5. Perpanjangan Pinjam

Aturan contoh: perpanjangan hanya sekali dan hanya sebelum jatuh tempo, agar anggota tidak memakai perpanjangan untuk menghindari denda.

public function renew(Loan $loan)
{
    abort_if($loan->returned_at !== null, 422, 'Peminjaman sudah selesai.');
    abort_if($loan->renewals >= 1, 422, 'Perpanjangan hanya boleh satu kali.');
    abort_if(today()->greaterThan($loan->due_at), 422, 'Sudah lewat jatuh tempo.');

    $loan->update([
        'due_at'   => $loan->due_at->copy()->addDays(Loan::LOAN_DAYS),
        'renewals' => $loan->renewals + 1,
    ]);

    return back()->with('success', 'Jatuh tempo diperpanjang.');
}

6. Daftar Terlambat dan Pengingat Otomatis

Petugas perlu daftar siapa saja yang masih menahan buku melewati jatuh tempo:

$overdue = Loan::with(['book', 'member'])
    ->whereNull('returned_at')
    ->whereDate('due_at', '<', today())
    ->orderBy('due_at')
    ->get();

Untuk pengingat H-1, buat command lalu daftarkan di routes/console.php. Di Laravel 11, jadwal tidak lagi ditulis di Kernel.php:

use Illuminate\Support\Facades\Schedule;

Schedule::command('loans:remind')->dailyAt('08:00');

Di dalam command, ambil peminjaman aktif dengan whereDate('due_at', today()->addDay()) lalu kirim email atau pesan. Jika pengirimannya lambat, lempar ke queue. Pastikan cron server menjalankan php artisan schedule:run setiap menit.

7. Laporan untuk Pengelola Perpustakaan

Data peminjaman yang tersimpan rapi bisa langsung diolah menjadi laporan. Contoh: sepuluh judul paling sering dipinjam dalam 30 hari terakhir, berguna untuk memutuskan judul mana yang perlu ditambah eksemplarnya.

$popular = Book::withCount(['loans' => fn ($q) => $q->where('borrowed_at', '>=', today()->subDays(30))])
    ->orderByDesc('loans_count')
    ->limit(10)
    ->get();

Laporan lain yang sering diminta: total denda per bulan (sum('fine') dikelompokkan per bulan returned_at) dan anggota yang paling sering terlambat. Semua bisa dibuat tanpa tabel tambahan karena riwayat peminjaman tidak pernah dihapus.

Kesalahan Umum dan Perbaikannya

  • Kolom boolean "dipinjam" di tabel buku. Gagal untuk judul dengan banyak eksemplar. Hitung dari copies dikurangi peminjaman aktif.
  • Meminjam tanpa transaksi dan lock. Eksemplar terakhir bisa keluar dua kali. Bungkus dengan DB::transaction dan lockForUpdate.
  • Denda negatif atau pecahan. Periksa urutan argumen diffInDays dan cast ke integer.
  • Menghapus data peminjaman lama. Riwayat dibutuhkan untuk laporan dan sengketa denda. Simpan, lalu filter di tampilan.
  • Accessor yang menjalankan query di halaman daftar. Ganti dengan withCount.

Checklist Sebelum Dipakai Petugas

  • Aturan lama pinjam, denda, dan batas pinjam disetujui pengelola perpustakaan.
  • Peminjaman bersamaan untuk eksemplar terakhir sudah diuji.
  • Denda tepat waktu, terlambat satu hari, dan terlambat lama sudah dicek.
  • Hak akses dibedakan antara petugas dan admin.
  • Cron untuk scheduler aktif di server.
  • Backup database harian berjalan.

Struktur ini mudah dikembangkan menjadi reservasi buku, kartu anggota dengan barcode, atau laporan buku terpopuler tanpa merombak tabel. Jika perpustakaan ini bagian dari sistem yang lebih besar, pola yang sama bisa digabungkan dengan modul di sistem informasi sekolah modern.

Ingin langsung mempraktikkannya?

Buat akun gratis untuk mengunduh contoh aplikasi Laravel, menyimpan tutorial favorit, dan mencoba kursus. Gratis, bisa daftar dengan akun Google.

Daftar Gratis Lihat katalog aplikasi
Yudhi
Ditulis oleh
Yudhi
Founder & Lead Developer, GudangCode

Yudhi adalah founder GudangCode dan developer Laravel dengan pengalaman membangun puluhan sistem informasi bisnis siap pakai — mulai dari POS, HRIS, hingga aplikasi manajemen. Ia menulis panduan dan artikel di GudangCode untuk membantu developer Indonesia menjalankan, memahami, dan men-deploy source code Laravel dengan benar.

Laravel PHP MySQL Sistem Informasi Bisnis
Lihat semua artikel Yudhi
Sistem Informasi Perpustakaan Sistem Informasi
Bacaan Paling Dicari
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →