đŸ’ģ Pemrograman Web 2
🎓 Pertemuan
Pertemuan 12: CRUD Buku Dengan Laravel

MODUL PEMROGRAMAN WEBSITE 2

Mata Kuliah: Pemrograman Website 2
Kode MK: INF2419
SKS: 3 (Praktikum)
Semester: Genap 2025/2026
Program Studi: Informatika
Fakultas: FEBI / Saintek
Universitas: UIN K.H. Abdurrahman Wahid Pekalongan

Dosen Pengampu: Mohammad Reza Maulana, M.Kom
NIP: 199110082025051002

Pertemuan: 12 dari 16
Durasi: 150 menit (3 × 50 menit)
Studi Kasus Berkelanjutan: Sistem Manajemen Perpustakaan


PERTEMUAN 12

CRUD BUKU DENGAN LARAVEL

A. INFORMASI PERTEMUAN

AspekKeterangan
Capaian Pembelajaran Lulusan (CPL)CPL06: Mempunyai pengetahuan dalam mengembangkan algoritma/metode yang diimplementasikan dalam perangkat lunak.
Capaian Pembelajaran Mata Kuliah (CPMK)CPMK06.1: Mampu mengimplementasikan backend web menggunakan PHP dan Laravel yang terintegrasi dengan database.
Sub-CPMKSub-CPMK06.1.2: Mengimplementasikan CRUD menggunakan Laravel dan ORM (Eloquent).
Indikator PencapaianMahasiswa mampu:
1. Membuat form untuk input data buku
2. Implementasi validasi form dengan Laravel validation
3. Menyimpan data buku ke database (Create)
4. Menampilkan data buku dari database (Read)
5. Membuat form edit dan update data buku (Update)
6. Menghapus data buku dari database (Delete)
7. Implementasi CSRF protection
8. Menampilkan flash messages
9. Error handling untuk CRUD operations
Alokasi Waktuâ€ĸ Teori: 60 menit
â€ĸ Praktikum: 90 menit
â€ĸ Total: 150 menit (3 × 50 menit)

B. PENDAHULUAN

1. Deskripsi Singkat

Pertemuan keduabelas ini fokus pada implementasi lengkap operasi CRUD (Create, Read, Update, Delete) untuk data buku menggunakan Laravel 12. Mahasiswa akan mempelajari cara membuat form input, validasi data, menyimpan ke database, menampilkan, mengupdate, dan menghapus data dengan memanfaatkan Eloquent ORM dan fitur-fitur Laravel seperti Form Request validation, CSRF protection, dan flash messages.

2. Keterkaitan dengan Pertemuan Lain

Pertemuan ini merupakan kelanjutan dari pertemuan-pertemuan sebelumnya:

  • Pertemuan 7: CRUD native PHP/MySQL - konsep dasar CRUD
  • Pertemuan 10: Migration & Model - struktur database dan Eloquent
  • Pertemuan 11: Controller & View - menampilkan data (Read operation)
  • Pertemuan 12: [SEKARANG] Implementasi lengkap CRUD Buku
  • Pertemuan 13: CRUD Anggota - replikasi pola yang sama
  • Pertemuan 14: Transaksi - relasi antar tabel dengan CRUD

Progress MVC + CRUD:

Pertemuan 10 → Model (M)
Pertemuan 11 → Controller & View (C & V) - Read
Pertemuan 12 → CRUD Complete (Create, Update, Delete)

3. Manfaat Pembelajaran

  1. Mahir membuat form dengan Laravel Blade
  2. Mampu implementasi validasi data yang robust
  3. Menguasai operasi database dengan Eloquent
  4. Memahami security (CSRF protection)
  5. Dapat memberikan feedback ke user (flash messages)
  6. Siap mengembangkan aplikasi web CRUD lainnya

4. Relevansi dengan Studi Kasus

Dalam sistem perpustakaan, CRUD Buku digunakan untuk:

  • Create: Menambah buku baru ke koleksi perpustakaan
  • Read: Melihat daftar dan detail buku (sudah di pertemuan 11)
  • Update: Mengubah informasi buku (harga, stok, deskripsi)
  • Delete: Menghapus buku yang sudah tidak tersedia

C. MATERI TEORI

1. CRUD Operations Overview

a. Apa itu CRUD?

CRUD adalah akronim dari Create, Read, Update, Delete - empat operasi dasar dalam manajemen data.

OperationSQLHTTP MethodLaravel MethodKeterangan
CreateINSERTPOSTstore()Menambah data baru
ReadSELECTGETindex(), show()Menampilkan data
UpdateUPDATEPUT/PATCHupdate()Mengubah data
DeleteDELETEDELETEdestroy()Menghapus data

b. Resource Controller Methods

Laravel Resource Controller menyediakan 7 methods standar untuk CRUD:

class BukuController extends Controller
{
    public function index()     // GET    /buku           - Tampil semua
    public function create()    // GET    /buku/create    - Form tambah
    public function store()     // POST   /buku           - Simpan data
    public function show($id)   // GET    /buku/{id}      - Tampil detail
    public function edit($id)   // GET    /buku/{id}/edit - Form edit
    public function update($id) // PUT    /buku/{id}      - Update data
    public function destroy($id)// DELETE /buku/{id}      - Hapus data
}

c. CRUD Workflow

CREATE FLOW:
User → Form Create (create) → Submit → Validation → Store to DB → Redirect

READ FLOW:
User → Request → Controller → Model → Database → View → Response

UPDATE FLOW:
User → Form Edit (edit) → Submit → Validation → Update DB → Redirect

DELETE FLOW:
User → Confirm → Destroy → Delete from DB → Redirect

2. Form Handling di Laravel

a. Membuat Form dengan Blade

Form Create/Edit Structure:

<form action="{{ route('buku.store') }}" method="POST">
    @csrf
    
    <div class="mb-3">
        <label for="judul" class="form-label">Judul Buku</label>
        <input type="text" 
               name="judul" 
               id="judul" 
               class="form-control @error('judul') is-invalid @enderror"
               value="{{ old('judul') }}">
        @error('judul')
            <div class="invalid-feedback">{{ $message }}</div>
        @enderror
    </div>
    
    <button type="submit" class="btn btn-primary">Simpan</button>
</form>

Penjelasan Komponen:

  1. Action & Method:
<form action="{{ route('buku.store') }}" method="POST">
  • action: URL tujuan submit (gunakan named route)
  • method: HTTP method (POST untuk create, POST untuk update dengan spoofing)
  1. CSRF Token:
@csrf
  • Wajib untuk semua form POST/PUT/DELETE
  • Proteksi dari Cross-Site Request Forgery
  • Laravel otomatis validasi token ini
  1. Method Spoofing (untuk Update/Delete):
@method('PUT')    <!-- Untuk update -->
@method('DELETE') <!-- Untuk delete -->
  • HTML form hanya support GET dan POST
  • Laravel "spoof" method PUT/DELETE lewat hidden input
  1. Old Input:
value="{{ old('judul') }}"
  • Menampilkan input lama jika validasi gagal
  • User tidak perlu input ulang semua field
  1. Error Messages:
@error('judul')
    <div class="invalid-feedback">{{ $message }}</div>
@enderror
  • Menampilkan pesan error spesifik per field
  • Terintegrasi dengan Laravel validation
  1. Conditional CSS Class:
class="form-control @error('judul') is-invalid @enderror"
  • Tambah class is-invalid jika ada error
  • Bootstrap otomatis styling error state

b. Form Elements

Text Input:

<input type="text" name="judul" class="form-control" value="{{ old('judul') }}">

Number Input:

<input type="number" name="stok" class="form-control" value="{{ old('stok', 0) }}">

Textarea:

<textarea name="deskripsi" class="form-control" rows="4">{{ old('deskripsi') }}</textarea>

Select Dropdown:

<select name="kategori" class="form-control">
    <option value="">-- Pilih Kategori --</option>
    <option value="Programming" {{ old('kategori') == 'Programming' ? 'selected' : '' }}>
        Programming
    </option>
    <option value="Database" {{ old('kategori') == 'Database' ? 'selected' : '' }}>
        Database
    </option>
</select>

Date Input:

<input type="date" name="tanggal_terbit" class="form-control" value="{{ old('tanggal_terbit') }}">

File Upload (akan dipelajari lanjutan):

<input type="file" name="cover" class="form-control">

3. Laravel Validation

a. Validation Rules

Laravel menyediakan banyak built-in validation rules:

Validation di Controller:

$validated = $request->validate([
    'judul' => 'required|string|max:200',
    'kategori' => 'required|in:Programming,Database,Web Design,Networking,Data Science',
    'pengarang' => 'required|string|max:100',
    'penerbit' => 'required|string|max:100',
    'tahun_terbit' => 'required|integer|min:1900|max:' . date('Y'),
    'isbn' => 'nullable|string|max:20|unique:buku,isbn',
    'harga' => 'required|numeric|min:0',
    'stok' => 'required|integer|min:0',
    'deskripsi' => 'nullable|string',
]);

Common Validation Rules:

RuleDeskripsiContoh
requiredField wajib diisi'judul' => 'required'
nullableField boleh kosong'isbn' => 'nullable'
stringHarus string'judul' => 'string'
integerHarus integer'stok' => 'integer'
numericHarus angka'harga' => 'numeric'
emailFormat email valid'email' => 'email'
min:valueNilai/panjang minimum'stok' => 'min:0'
max:valueNilai/panjang maksimum'judul' => 'max:200'
between:min,maxNilai antara min-max'harga' => 'between:0,1000000'
in:foo,barNilai harus dari list'kategori' => 'in:A,B,C'
unique:table,columnNilai harus unik'isbn' => 'unique:buku,isbn'
exists:table,columnNilai harus ada di DB'kategori_id' => 'exists:kategoris,id'
dateFormat tanggal valid'tanggal' => 'date'
after:dateTanggal setelah'tanggal_kembali' => 'after:tanggal_pinjam'
before:dateTanggal sebelum'tanggal_lahir' => 'before:today'

b. Custom Error Messages

Default Error Messages:

$request->validate([
    'judul' => 'required|max:200',
]);
// Error: "The judul field is required."

Custom Messages:

$request->validate([
    'judul' => 'required|max:200',
    'harga' => 'required|numeric|min:0',
], [
    'judul.required' => 'Judul buku wajib diisi.',
    'judul.max' => 'Judul buku maksimal 200 karakter.',
    'harga.required' => 'Harga buku wajib diisi.',
    'harga.numeric' => 'Harga harus berupa angka.',
    'harga.min' => 'Harga tidak boleh negatif.',
]);

Custom Attribute Names:

$request->validate([
    'judul' => 'required',
    'pengarang' => 'required',
], [], [
    'judul' => 'judul buku',
    'pengarang' => 'nama pengarang',
]);
// Error: "The judul buku field is required."

c. Validation dengan Form Request

Generate Form Request:

php artisan make:request StoreBukuRequest

Form Request Class:

File: app/Http/Requests/StoreBukuRequest.php

<?php
 
namespace App\Http\Requests;
 
use Illuminate\Foundation\Http\FormRequest;
 
class StoreBukuRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true; // Set true untuk allow semua user
    }
 
    /**
     * Get the validation rules that apply to the request.
     */
    public function rules(): array
    {
        return [
            'kode_buku' => 'required|string|max:20|unique:buku,kode_buku',
            'judul' => 'required|string|max:200',
            'kategori' => 'required|in:Programming,Database,Web Design,Networking,Data Science',
            'pengarang' => 'required|string|max:100',
            'penerbit' => 'required|string|max:100',
            'tahun_terbit' => 'required|integer|min:1900|max:' . date('Y'),
            'isbn' => 'nullable|string|max:20',
            'harga' => 'required|numeric|min:0',
            'stok' => 'required|integer|min:0',
            'deskripsi' => 'nullable|string',
            'bahasa' => 'required|string|max:20',
        ];
    }
 
    /**
     * Get custom error messages.
     */
    public function messages(): array
    {
        return [
            'kode_buku.required' => 'Kode buku wajib diisi.',
            'kode_buku.unique' => 'Kode buku sudah digunakan.',
            'judul.required' => 'Judul buku wajib diisi.',
            'kategori.required' => 'Kategori wajib dipilih.',
            'kategori.in' => 'Kategori tidak valid.',
            'harga.required' => 'Harga buku wajib diisi.',
            'harga.numeric' => 'Harga harus berupa angka.',
            'stok.integer' => 'Stok harus berupa angka bulat.',
        ];
    }
}

Usage di Controller:

public function store(StoreBukuRequest $request)
{
    // Validasi otomatis dijalankan
    // Jika gagal, otomatis redirect back dengan error
    
    Buku::create($request->validated());
    
    return redirect()->route('buku.index')
                     ->with('success', 'Buku berhasil ditambahkan!');
}

Keuntungan Form Request:

  • ✅ Separation of concerns (validasi terpisah dari controller)
  • ✅ Reusable (bisa dipakai di multiple methods)
  • ✅ Clean controller code
  • ✅ Mudah di-test
  • ✅ Authorization logic terintegrasi

4. CSRF Protection

a. Apa itu CSRF?

CSRF (Cross-Site Request Forgery) adalah serangan dimana attacker membuat user melakukan aksi yang tidak diinginkan pada aplikasi dimana user ter-autentikasi.

Contoh Serangan CSRF:

<!-- Situs jahat (evil.com) -->
<form action="https://perpustakaan.com/buku/1" method="POST">
    @method('DELETE')
    <input type="hidden" name="confirm" value="yes">
</form>
<script>
    document.forms[0].submit(); // Auto submit
</script>

Jika user yang login ke perpustakaan.com mengunjungi evil.com, buku dengan ID 1 bisa terhapus tanpa sepengetahuan user!

b. CSRF Protection di Laravel

Laravel protect semua form POST/PUT/DELETE dengan CSRF token.

Cara Kerja:

  1. Laravel generate unique token per session
  2. Token disimpan di session
  3. Form harus include token via @csrf
  4. Laravel validasi token saat form di-submit
  5. Jika token tidak cocok, request ditolak (419 error)

Implementasi:

<form action="{{ route('buku.store') }}" method="POST">
    @csrf
    <!-- Form fields -->
</form>

Laravel compile @csrf menjadi:

<input type="hidden" name="_token" value="random_token_string">

Exclude Routes dari CSRF (jarang dipakai):

File: app/Http/Middleware/VerifyCsrfToken.php

protected $except = [
    'webhook/*', // Route yang di-exclude
];

5. Flash Messages

a. Apa itu Flash Messages?

Flash Messages adalah pesan sementara yang ditampilkan setelah user melakukan aksi (create, update, delete). Pesan ini hanya muncul sekali di page load berikutnya, kemudian hilang.

b. Set Flash Messages

Di Controller:

// Success message
return redirect()->route('buku.index')
                 ->with('success', 'Buku berhasil ditambahkan!');
 
// Error message
return redirect()->back()
                 ->with('error', 'Gagal menambahkan buku!');
 
// Info message
return redirect()->route('buku.show', $buku->id)
                 ->with('info', 'Data buku telah diupdate.');
 
// Warning message
return redirect()->back()
                 ->with('warning', 'Stok buku hampir habis!');
 
// Multiple messages
return redirect()->route('buku.index')
                 ->with([
                     'success' => 'Buku berhasil ditambahkan!',
                     'info' => 'Total buku: ' . Buku::count()
                 ]);

c. Display Flash Messages

Di Blade Layout:

File: resources/views/layouts/app.blade.php

<main class="py-4">
    <div class="container">
        {{-- Flash Messages --}}
        @if (session('success'))
            <div class="alert alert-success alert-dismissible fade show" role="alert">
                <i class="bi bi-check-circle-fill"></i>
                {{ session('success') }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        @endif
        
        @if (session('error'))
            <div class="alert alert-danger alert-dismissible fade show" role="alert">
                <i class="bi bi-exclamation-triangle-fill"></i>
                {{ session('error') }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        @endif
        
        @if (session('info'))
            <div class="alert alert-info alert-dismissible fade show" role="alert">
                <i class="bi bi-info-circle-fill"></i>
                {{ session('info') }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        @endif
        
        @if (session('warning'))
            <div class="alert alert-warning alert-dismissible fade show" role="alert">
                <i class="bi bi-exclamation-circle-fill"></i>
                {{ session('warning') }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        @endif
        
        @yield('content')
    </div>
</main>

d. Auto-hide Flash Messages dengan JavaScript

@push('scripts')
<script>
    // Auto hide alerts after 5 seconds
    setTimeout(function() {
        let alerts = document.querySelectorAll('.alert');
        alerts.forEach(function(alert) {
            let bsAlert = new bootstrap.Alert(alert);
            bsAlert.close();
        });
    }, 5000);
</script>
@endpush

6. Eloquent CRUD Methods

a. Create Operations

Method 1: save()

$buku = new Buku();
$buku->judul = $request->judul;
$buku->kategori = $request->kategori;
$buku->harga = $request->harga;
$buku->save(); // INSERT ke database

Method 2: create() - Mass Assignment

Buku::create([
    'judul' => $request->judul,
    'kategori' => $request->kategori,
    'harga' => $request->harga,
]);
// Perlu $fillable di Model

Method 3: create() dengan validated()

Buku::create($request->validated());
// Best practice dengan Form Request

b. Read Operations

// Get all records
$bukus = Buku::all();
 
// Get with conditions
$bukus = Buku::where('kategori', 'Programming')->get();
 
// Find by primary key
$buku = Buku::find($id);
 
// Find or throw 404
$buku = Buku::findOrFail($id);
 
// Get first record
$buku = Buku::first();
 
// Ordering
$bukus = Buku::orderBy('judul', 'asc')->get();
 
// Limit
$bukus = Buku::take(10)->get();

c. Update Operations

Method 1: save()

$buku = Buku::findOrFail($id);
$buku->judul = $request->judul;
$buku->harga = $request->harga;
$buku->save(); // UPDATE database

Method 2: update() - Mass Assignment

$buku = Buku::findOrFail($id);
$buku->update([
    'judul' => $request->judul,
    'harga' => $request->harga,
]);

Method 3: update() dengan validated()

$buku = Buku::findOrFail($id);
$buku->update($request->validated());
// Best practice

d. Delete Operations

Method 1: delete()

$buku = Buku::findOrFail($id);
$buku->delete(); // DELETE from database

Method 2: destroy()

Buku::destroy($id); // Delete by ID
 
Buku::destroy([1, 2, 3]); // Delete multiple IDs

Method 3: Delete with condition

Buku::where('stok', 0)->delete(); // Delete semua buku stok habis

7. Best Practices CRUD

a. Controller Best Practices

✅ DO:

// 1. Gunakan Form Request
public function store(StoreBukuRequest $request)
{
    Buku::create($request->validated());
    return redirect()->route('buku.index')
                     ->with('success', 'Buku berhasil ditambahkan!');
}
 
// 2. Handle exceptions
public function destroy($id)
{
    try {
        $buku = Buku::findOrFail($id);
        $buku->delete();
        return redirect()->route('buku.index')
                         ->with('success', 'Buku berhasil dihapus!');
    } catch (\Exception $e) {
        return redirect()->route('buku.index')
                         ->with('error', 'Gagal menghapus buku!');
    }
}
 
// 3. Use route model binding
public function update(UpdateBukuRequest $request, Buku $buku)
{
    $buku->update($request->validated());
    return redirect()->route('buku.show', $buku)
                     ->with('success', 'Buku berhasil diupdate!');
}

❌ DON'T:

// 1. Validasi manual di controller
public function store(Request $request)
{
    if (empty($request->judul)) {
        return back()->with('error', 'Judul wajib diisi');
    }
    if (strlen($request->judul) > 200) {
        return back()->with('error', 'Judul terlalu panjang');
    }
    // ... banyak validasi manual
}
 
// 2. Tanpa error handling
public function destroy($id)
{
    Buku::find($id)->delete(); // Crash jika ID tidak ada
    return redirect()->route('buku.index');
}

b. Validation Best Practices

✅ DO:

// 1. Pisahkan ke Form Request
php artisan make:request StoreBukuRequest
 
// 2. Validation untuk unique dengan ignore saat update
public function rules(): array
{
    return [
        'isbn' => 'nullable|unique:buku,isbn,' . $this->buku?->id,
        // Ignore current record saat update
    ];
}
 
// 3. Conditional validation
public function rules(): array
{
    $rules = [
        'judul' => 'required|max:200',
    ];
    
    if ($this->isMethod('post')) {
        $rules['kode_buku'] = 'required|unique:buku,kode_buku';
    } else {
        $rules['kode_buku'] = 'required|unique:buku,kode_buku,' . $this->buku->id;
    }
    
    return $rules;
}

c. Security Best Practices

✅ DO:

// 1. Always use @csrf
<form method="POST">
    @csrf
    @method('PUT')
</form>
 
// 2. Use $fillable di Model
protected $fillable = ['judul', 'harga', 'stok'];
 
// 3. Validate all input
$validated = $request->validate([...]);
 
// 4. Use findOrFail untuk auto 404
$buku = Buku::findOrFail($id);

❌ DON'T:

// 1. Tanpa CSRF
<form method="POST">
    <!-- Missing @csrf - vulnerable! -->
</form>
 
// 2. Mass assignment tanpa protection
protected $guarded = []; // Dangerous!
 
// 3. Direct input ke database
Buku::create($request->all()); // All input accepted!
 
// 4. Suppress errors
@Buku::find($id)->delete(); // Silent fail

D. PRAKTIKUM

1. Tujuan Praktikum

  1. Membuat form create untuk input buku baru
  2. Implementasi store method dengan validation
  3. Membuat form edit untuk update buku
  4. Implementasi update method
  5. Implementasi delete method
  6. Menampilkan flash messages
  7. Error handling yang baik

2. PRAKTIKUM 1: Form Create Buku

Tujuan

Membuat form untuk menambah buku baru dengan validation.

Langkah-langkah

a. Generate Form Request

php artisan make:request StoreBukuRequest
php artisan make:request UpdateBukuRequest

b. Edit StoreBukuRequest

File: app/Http/Requests/StoreBukuRequest.php

<?php
 
namespace App\Http\Requests;
 
use Illuminate\Foundation\Http\FormRequest;
 
class StoreBukuRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }
 
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'kode_buku' => 'required|string|max:20|unique:buku,kode_buku',
            'judul' => 'required|string|max:200',
            'kategori' => 'required|in:Programming,Database,Web Design,Networking,Data Science',
            'pengarang' => 'required|string|max:100',
            'penerbit' => 'required|string|max:100',
            'tahun_terbit' => 'required|integer|min:1900|max:' . date('Y'),
            'isbn' => 'nullable|string|max:20',
            'harga' => 'required|numeric|min:0',
            'stok' => 'required|integer|min:0',
            'deskripsi' => 'nullable|string',
            'bahasa' => 'required|string|max:20',
        ];
    }
 
    /**
     * Get custom error messages.
     */
    public function messages(): array
    {
        return [
            'kode_buku.required' => 'Kode buku wajib diisi.',
            'kode_buku.unique' => 'Kode buku sudah digunakan.',
            'kode_buku.max' => 'Kode buku maksimal 20 karakter.',
            'judul.required' => 'Judul buku wajib diisi.',
            'judul.max' => 'Judul buku maksimal 200 karakter.',
            'kategori.required' => 'Kategori wajib dipilih.',
            'kategori.in' => 'Kategori tidak valid.',
            'pengarang.required' => 'Nama pengarang wajib diisi.',
            'penerbit.required' => 'Nama penerbit wajib diisi.',
            'tahun_terbit.required' => 'Tahun terbit wajib diisi.',
            'tahun_terbit.integer' => 'Tahun terbit harus berupa angka.',
            'tahun_terbit.min' => 'Tahun terbit tidak valid.',
            'tahun_terbit.max' => 'Tahun terbit tidak boleh melebihi tahun sekarang.',
            'isbn.max' => 'ISBN maksimal 20 karakter.',
            'harga.required' => 'Harga buku wajib diisi.',
            'harga.numeric' => 'Harga harus berupa angka.',
            'harga.min' => 'Harga tidak boleh negatif.',
            'stok.required' => 'Stok wajib diisi.',
            'stok.integer' => 'Stok harus berupa angka bulat.',
            'stok.min' => 'Stok tidak boleh negatif.',
            'bahasa.required' => 'Bahasa wajib diisi.',
        ];
    }
 
    /**
     * Get custom attribute names.
     */
    public function attributes(): array
    {
        return [
            'kode_buku' => 'kode buku',
            'judul' => 'judul buku',
            'kategori' => 'kategori',
            'pengarang' => 'nama pengarang',
            'penerbit' => 'nama penerbit',
            'tahun_terbit' => 'tahun terbit',
            'isbn' => 'ISBN',
            'harga' => 'harga',
            'stok' => 'stok',
            'bahasa' => 'bahasa',
        ];
    }
}

c. Update BukuController - Method create()

File: app/Http/Controllers/BukuController.php

/**
 * Show the form for creating a new resource.
 */
public function create()
{
    return view('buku.create');
}

d. Buat View Create

File: resources/views/buku/create.blade.php

@extends('layouts.app')
 
@section('title', 'Tambah Buku')
 
@section('content')
<div class="row justify-content-center">
    <div class="col-md-10">
        <div class="card">
            <div class="card-header bg-primary text-white">
                <h4 class="mb-0">
                    <i class="bi bi-plus-circle"></i>
                    Tambah Buku Baru
                </h4>
            </div>
            <div class="card-body">
                <form action="{{ route('buku.store') }}" method="POST">
                    @csrf
                    
                    <div class="row">
                        {{-- Kode Buku --}}
                        <div class="col-md-4 mb-3">
                            <label for="kode_buku" class="form-label">
                                Kode Buku <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="kode_buku" 
                                   id="kode_buku" 
                                   class="form-control @error('kode_buku') is-invalid @enderror"
                                   value="{{ old('kode_buku') }}"
                                   placeholder="Contoh: BK-001">
                            @error('kode_buku')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Judul --}}
                        <div class="col-md-8 mb-3">
                            <label for="judul" class="form-label">
                                Judul Buku <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="judul" 
                                   id="judul" 
                                   class="form-control @error('judul') is-invalid @enderror"
                                   value="{{ old('judul') }}"
                                   placeholder="Masukkan judul buku">
                            @error('judul')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    <div class="row">
                        {{-- Kategori --}}
                        <div class="col-md-4 mb-3">
                            <label for="kategori" class="form-label">
                                Kategori <span class="text-danger">*</span>
                            </label>
                            <select name="kategori" 
                                    id="kategori" 
                                    class="form-select @error('kategori') is-invalid @enderror">
                                <option value="">-- Pilih Kategori --</option>
                                <option value="Programming" {{ old('kategori') == 'Programming' ? 'selected' : '' }}>
                                    Programming
                                </option>
                                <option value="Database" {{ old('kategori') == 'Database' ? 'selected' : '' }}>
                                    Database
                                </option>
                                <option value="Web Design" {{ old('kategori') == 'Web Design' ? 'selected' : '' }}>
                                    Web Design
                                </option>
                                <option value="Networking" {{ old('kategori') == 'Networking' ? 'selected' : '' }}>
                                    Networking
                                </option>
                                <option value="Data Science" {{ old('kategori') == 'Data Science' ? 'selected' : '' }}>
                                    Data Science
                                </option>
                            </select>
                            @error('kategori')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Pengarang --}}
                        <div class="col-md-4 mb-3">
                            <label for="pengarang" class="form-label">
                                Pengarang <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="pengarang" 
                                   id="pengarang" 
                                   class="form-control @error('pengarang') is-invalid @enderror"
                                   value="{{ old('pengarang') }}"
                                   placeholder="Nama pengarang">
                            @error('pengarang')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Penerbit --}}
                        <div class="col-md-4 mb-3">
                            <label for="penerbit" class="form-label">
                                Penerbit <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="penerbit" 
                                   id="penerbit" 
                                   class="form-control @error('penerbit') is-invalid @enderror"
                                   value="{{ old('penerbit') }}"
                                   placeholder="Nama penerbit">
                            @error('penerbit')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    <div class="row">
                        {{-- Tahun Terbit --}}
                        <div class="col-md-3 mb-3">
                            <label for="tahun_terbit" class="form-label">
                                Tahun Terbit <span class="text-danger">*</span>
                            </label>
                            <input type="number" 
                                   name="tahun_terbit" 
                                   id="tahun_terbit" 
                                   class="form-control @error('tahun_terbit') is-invalid @enderror"
                                   value="{{ old('tahun_terbit', date('Y')) }}"
                                   min="1900"
                                   max="{{ date('Y') }}">
                            @error('tahun_terbit')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- ISBN --}}
                        <div class="col-md-3 mb-3">
                            <label for="isbn" class="form-label">
                                ISBN
                            </label>
                            <input type="text" 
                                   name="isbn" 
                                   id="isbn" 
                                   class="form-control @error('isbn') is-invalid @enderror"
                                   value="{{ old('isbn') }}"
                                   placeholder="978-xxx-xxx">
                            @error('isbn')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Bahasa --}}
                        <div class="col-md-2 mb-3">
                            <label for="bahasa" class="form-label">
                                Bahasa <span class="text-danger">*</span>
                            </label>
                            <select name="bahasa" 
                                    id="bahasa" 
                                    class="form-select @error('bahasa') is-invalid @enderror">
                                <option value="Indonesia" {{ old('bahasa', 'Indonesia') == 'Indonesia' ? 'selected' : '' }}>
                                    Indonesia
                                </option>
                                <option value="Inggris" {{ old('bahasa') == 'Inggris' ? 'selected' : '' }}>
                                    Inggris
                                </option>
                            </select>
                            @error('bahasa')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Harga --}}
                        <div class="col-md-2 mb-3">
                            <label for="harga" class="form-label">
                                Harga <span class="text-danger">*</span>
                            </label>
                            <input type="number" 
                                   name="harga" 
                                   id="harga" 
                                   class="form-control @error('harga') is-invalid @enderror"
                                   value="{{ old('harga', 0) }}"
                                   min="0"
                                   step="1000">
                            @error('harga')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Stok --}}
                        <div class="col-md-2 mb-3">
                            <label for="stok" class="form-label">
                                Stok <span class="text-danger">*</span>
                            </label>
                            <input type="number" 
                                   name="stok" 
                                   id="stok" 
                                   class="form-control @error('stok') is-invalid @enderror"
                                   value="{{ old('stok', 0) }}"
                                   min="0">
                            @error('stok')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    {{-- Deskripsi --}}
                    <div class="mb-3">
                        <label for="deskripsi" class="form-label">Deskripsi</label>
                        <textarea name="deskripsi" 
                                  id="deskripsi" 
                                  rows="4" 
                                  class="form-control @error('deskripsi') is-invalid @enderror"
                                  placeholder="Deskripsi singkat tentang buku (opsional)">{{ old('deskripsi') }}</textarea>
                        @error('deskripsi')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>
                    
                    <hr>
                    
                    {{-- Buttons --}}
                    <div class="d-flex justify-content-between">
                        <a href="{{ route('buku.index') }}" class="btn btn-secondary">
                            <i class="bi bi-arrow-left"></i> Kembali
                        </a>
                        <button type="submit" class="btn btn-primary">
                            <i class="bi bi-save"></i> Simpan Buku
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
@endsection
 
@push('scripts')
<script>
    // Auto format harga dengan thousand separator
    document.getElementById('harga').addEventListener('blur', function() {
        let value = this.value.replace(/\D/g, '');
        this.value = value;
    });
</script>
@endpush

e. Test Form Create

  1. Akses: http://localhost:8000/buku/create
  2. Verifikasi:
    • Form muncul dengan semua field
    • Required fields ada tanda *
    • Dropdown kategori dan bahasa berfungsi
    • Placeholder text muncul

3. PRAKTIKUM 2: Store Method (Create Operation)

Tujuan

Implementasi method untuk menyimpan data buku ke database.

Langkah-langkah

a. Update BukuController - Method store()

File: app/Http/Controllers/BukuController.php

use App\Http\Requests\StoreBukuRequest;
 
/**
 * Store a newly created resource in storage.
 */
public function store(StoreBukuRequest $request)
{
    try {
        // Create buku baru dengan validated data
        Buku::create($request->validated());
        
        // Redirect dengan success message
        return redirect()->route('buku.index')
                         ->with('success', 'Buku berhasil ditambahkan!');
                         
    } catch (\Exception $e) {
        // Redirect dengan error message jika gagal
        return redirect()->back()
                         ->withInput()
                         ->with('error', 'Gagal menambahkan buku: ' . $e->getMessage());
    }
}

b. Test Store Operation

  1. Buka form create: http://localhost:8000/buku/create

  2. Test 1 - Submit form kosong:

    • Klik "Simpan Buku"
    • Verifikasi: Error validation muncul untuk required fields
  3. Test 2 - Submit dengan data valid:

    • Isi semua required fields:
      • Kode Buku: BK-TEST-001
      • Judul: Testing Laravel CRUD
      • Kategori: Programming
      • Pengarang: John Doe
      • Penerbit: Test Publisher
      • Tahun: 2024
      • Harga: 150000
      • Stok: 10
      • Bahasa: Indonesia
    • Klik "Simpan Buku"
    • Verifikasi:
      • Redirect ke halaman index
      • Flash message "Buku berhasil ditambahkan!" muncul
      • Buku baru muncul di list
  4. Test 3 - Validation errors:

    • Kode Buku: BK-TEST-001 (sama dengan sebelumnya - test unique)
    • Harga: -5000 (test min:0)
    • Tahun: 2050 (test max tahun sekarang)
    • Klik submit
    • Verifikasi: Error validation muncul sesuai rules
  5. Test 4 - Old input preserved:

    • Isi beberapa field
    • Submit dengan error (misal judul kosong)
    • Verifikasi: Field yang sudah diisi tetap ada (old input)

c. Cek di Database

php artisan tinker
>>> \App\Models\Buku::where('kode_buku', 'BK-TEST-001')->first()
>>> \App\Models\Buku::latest()->first()

4. PRAKTIKUM 3: Form Edit & Update Operation

Tujuan

Membuat form edit dan implementasi update data buku.

Langkah-langkah

a. Edit UpdateBukuRequest

File: app/Http/Requests/UpdateBukuRequest.php

<?php
 
namespace App\Http\Requests;
 
use Illuminate\Foundation\Http\FormRequest;
 
class UpdateBukuRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }
 
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        // Get buku ID from route parameter
        $bukuId = $this->route('buku');
        
        return [
            'kode_buku' => 'required|string|max:20|unique:buku,kode_buku,' . $bukuId,
            'judul' => 'required|string|max:200',
            'kategori' => 'required|in:Programming,Database,Web Design,Networking,Data Science',
            'pengarang' => 'required|string|max:100',
            'penerbit' => 'required|string|max:100',
            'tahun_terbit' => 'required|integer|min:1900|max:' . date('Y'),
            'isbn' => 'nullable|string|max:20',
            'harga' => 'required|numeric|min:0',
            'stok' => 'required|integer|min:0',
            'deskripsi' => 'nullable|string',
            'bahasa' => 'required|string|max:20',
        ];
    }
 
    /**
     * Get custom error messages.
     */
    public function messages(): array
    {
        return [
            'kode_buku.required' => 'Kode buku wajib diisi.',
            'kode_buku.unique' => 'Kode buku sudah digunakan.',
            'judul.required' => 'Judul buku wajib diisi.',
            'kategori.required' => 'Kategori wajib dipilih.',
            'harga.required' => 'Harga buku wajib diisi.',
            'harga.numeric' => 'Harga harus berupa angka.',
            'stok.integer' => 'Stok harus berupa angka bulat.',
        ];
    }
}

b. Update BukuController - Method edit()

/**
 * Show the form for editing the specified resource.
 */
public function edit(string $id)
{
    $buku = Buku::findOrFail($id);
    return view('buku.edit', compact('buku'));
}

c. Buat View Edit

File: resources/views/buku/edit.blade.php

@extends('layouts.app')
 
@section('title', 'Edit Buku')
 
@section('content')
<div class="row justify-content-center">
    <div class="col-md-10">
        <div class="card">
            <div class="card-header bg-warning">
                <h4 class="mb-0">
                    <i class="bi bi-pencil-square"></i>
                    Edit Buku: {{ $buku->judul }}
                </h4>
            </div>
            <div class="card-body">
                <form action="{{ route('buku.update', $buku->id) }}" method="POST">
                    @csrf
                    @method('PUT')
                    
                    <div class="row">
                        {{-- Kode Buku --}}
                        <div class="col-md-4 mb-3">
                            <label for="kode_buku" class="form-label">
                                Kode Buku <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="kode_buku" 
                                   id="kode_buku" 
                                   class="form-control @error('kode_buku') is-invalid @enderror"
                                   value="{{ old('kode_buku', $buku->kode_buku) }}">
                            @error('kode_buku')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Judul --}}
                        <div class="col-md-8 mb-3">
                            <label for="judul" class="form-label">
                                Judul Buku <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="judul" 
                                   id="judul" 
                                   class="form-control @error('judul') is-invalid @enderror"
                                   value="{{ old('judul', $buku->judul) }}">
                            @error('judul')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    <div class="row">
                        {{-- Kategori --}}
                        <div class="col-md-4 mb-3">
                            <label for="kategori" class="form-label">
                                Kategori <span class="text-danger">*</span>
                            </label>
                            <select name="kategori" 
                                    id="kategori" 
                                    class="form-select @error('kategori') is-invalid @enderror">
                                <option value="">-- Pilih Kategori --</option>
                                @foreach(['Programming', 'Database', 'Web Design', 'Networking', 'Data Science'] as $kat)
                                    <option value="{{ $kat }}" 
                                            {{ old('kategori', $buku->kategori) == $kat ? 'selected' : '' }}>
                                        {{ $kat }}
                                    </option>
                                @endforeach
                            </select>
                            @error('kategori')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Pengarang --}}
                        <div class="col-md-4 mb-3">
                            <label for="pengarang" class="form-label">
                                Pengarang <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="pengarang" 
                                   id="pengarang" 
                                   class="form-control @error('pengarang') is-invalid @enderror"
                                   value="{{ old('pengarang', $buku->pengarang) }}">
                            @error('pengarang')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Penerbit --}}
                        <div class="col-md-4 mb-3">
                            <label for="penerbit" class="form-label">
                                Penerbit <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="penerbit" 
                                   id="penerbit" 
                                   class="form-control @error('penerbit') is-invalid @enderror"
                                   value="{{ old('penerbit', $buku->penerbit) }}">
                            @error('penerbit')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    <div class="row">
                        {{-- Tahun Terbit --}}
                        <div class="col-md-3 mb-3">
                            <label for="tahun_terbit" class="form-label">
                                Tahun Terbit <span class="text-danger">*</span>
                            </label>
                            <input type="number" 
                                   name="tahun_terbit" 
                                   id="tahun_terbit" 
                                   class="form-control @error('tahun_terbit') is-invalid @enderror"
                                   value="{{ old('tahun_terbit', $buku->tahun_terbit) }}"
                                   min="1900"
                                   max="{{ date('Y') }}">
                            @error('tahun_terbit')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- ISBN --}}
                        <div class="col-md-3 mb-3">
                            <label for="isbn" class="form-label">ISBN</label>
                            <input type="text" 
                                   name="isbn" 
                                   id="isbn" 
                                   class="form-control @error('isbn') is-invalid @enderror"
                                   value="{{ old('isbn', $buku->isbn) }}">
                            @error('isbn')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Bahasa --}}
                        <div class="col-md-2 mb-3">
                            <label for="bahasa" class="form-label">
                                Bahasa <span class="text-danger">*</span>
                            </label>
                            <select name="bahasa" 
                                    id="bahasa" 
                                    class="form-select @error('bahasa') is-invalid @enderror">
                                <option value="Indonesia" {{ old('bahasa', $buku->bahasa) == 'Indonesia' ? 'selected' : '' }}>
                                    Indonesia
                                </option>
                                <option value="Inggris" {{ old('bahasa', $buku->bahasa) == 'Inggris' ? 'selected' : '' }}>
                                    Inggris
                                </option>
                            </select>
                            @error('bahasa')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Harga --}}
                        <div class="col-md-2 mb-3">
                            <label for="harga" class="form-label">
                                Harga <span class="text-danger">*</span>
                            </label>
                            <input type="number" 
                                   name="harga" 
                                   id="harga" 
                                   class="form-control @error('harga') is-invalid @enderror"
                                   value="{{ old('harga', $buku->harga) }}"
                                   min="0"
                                   step="1000">
                            @error('harga')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Stok --}}
                        <div class="col-md-2 mb-3">
                            <label for="stok" class="form-label">
                                Stok <span class="text-danger">*</span>
                            </label>
                            <input type="number" 
                                   name="stok" 
                                   id="stok" 
                                   class="form-control @error('stok') is-invalid @enderror"
                                   value="{{ old('stok', $buku->stok) }}"
                                   min="0">
                            @error('stok')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    {{-- Deskripsi --}}
                    <div class="mb-3">
                        <label for="deskripsi" class="form-label">Deskripsi</label>
                        <textarea name="deskripsi" 
                                  id="deskripsi" 
                                  rows="4" 
                                  class="form-control @error('deskripsi') is-invalid @enderror">{{ old('deskripsi', $buku->deskripsi) }}</textarea>
                        @error('deskripsi')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>
                    
                    <hr>
                    
                    {{-- Buttons --}}
                    <div class="d-flex justify-content-between">
                        <a href="{{ route('buku.show', $buku->id) }}" class="btn btn-secondary">
                            <i class="bi bi-arrow-left"></i> Kembali
                        </a>
                        <button type="submit" class="btn btn-warning">
                            <i class="bi bi-save"></i> Update Buku
                        </button>
                    </div>
                </form>
            </div>
        </div>
        
        {{-- Info Update --}}
        <div class="card mt-3">
            <div class="card-body">
                <small class="text-muted">
                    <i class="bi bi-info-circle"></i>
                    <strong>Informasi:</strong><br />
                    - Buku ditambahkan: {{ $buku->created_at->format('d M Y H:i') }}<br />
                    - Terakhir diupdate: {{ $buku->updated_at->format('d M Y H:i') }}
                </small>
            </div>
        </div>
    </div>
</div>
@endsection

d. Update BukuController - Method update()

use App\Http\Requests\UpdateBukuRequest;
 
/**
 * Update the specified resource in storage.
 */
public function update(UpdateBukuRequest $request, string $id)
{
    try {
        $buku = Buku::findOrFail($id);
        
        // Update buku dengan validated data
        $buku->update($request->validated());
        
        // Redirect dengan success message
        return redirect()->route('buku.show', $buku->id)
                         ->with('success', 'Buku berhasil diupdate!');
                         
    } catch (\Exception $e) {
        // Redirect dengan error message jika gagal
        return redirect()->back()
                         ->withInput()
                         ->with('error', 'Gagal mengupdate buku: ' . $e->getMessage());
    }
}

e. Test Update Operation

  1. Dari halaman detail buku, klik "Edit Buku"

  2. URL: http://localhost:8000/buku/1/edit

  3. Verifikasi:

    • Form ter-isi dengan data buku yang ada
    • Semua field editable
  4. Test 1 - Update data:

    • Ubah beberapa field (misal: stok dari 10 jadi 15, harga naik)
    • Klik "Update Buku"
    • Verifikasi:
      • Redirect ke detail buku
      • Flash message "Buku berhasil diupdate!" muncul
      • Data ter-update di halaman detail
      • Timestamp "Terakhir diupdate" berubah
  5. Test 2 - Validation saat update:

    • Kosongkan field required
    • Submit
    • Verifikasi: Error validation muncul

5. PRAKTIKUM 4: Delete Operation

Tujuan

Implementasi method untuk menghapus data buku.

Langkah-langkah

a. Update BukuController - Method destroy()

/**
 * Remove the specified resource from storage.
 */
public function destroy(string $id)
{
    try {
        $buku = Buku::findOrFail($id);
        $judulBuku = $buku->judul;
        
        // Delete buku
        $buku->delete();
        
        // Redirect dengan success message
        return redirect()->route('buku.index')
                         ->with('success', "Buku '{$judulBuku}' berhasil dihapus!");
                         
    } catch (\Exception $e) {
        // Redirect dengan error message jika gagal
        return redirect()->back()
                         ->with('error', 'Gagal menghapus buku: ' . $e->getMessage());
    }
}

b. Update View Index - Tambah Delete Button

File: resources/views/buku/index.blade.php

Di dalam card buku, tambahkan form delete:

<div class="btn-group-vertical d-grid gap-2">
    <a href="{{ route('buku.show', $buku->id) }}" class="btn btn-sm btn-info text-white">
        <i class="bi bi-eye"></i> Detail
    </a>
    <a href="{{ route('buku.edit', $buku->id) }}" class="btn btn-sm btn-warning">
        <i class="bi bi-pencil"></i> Edit
    </a>
    
    {{-- Delete Button --}}
    <form action="{{ route('buku.destroy', $buku->id) }}" 
          method="POST" 
          class="d-inline"
          onsubmit="return confirm('Apakah Anda yakin ingin menghapus buku {{ $buku->judul }}?')">
        @csrf
        @method('DELETE')
        <button type="submit" class="btn btn-sm btn-danger w-100">
            <i class="bi bi-trash"></i> Hapus
        </button>
    </form>
</div>

c. Update View Show - Delete Button

File: resources/views/buku/show.blade.php

Form delete sudah ada di view show (di card Actions).

d. Test Delete Operation

  1. Test dari Index Page:

    • Buka halaman index buku
    • Klik button "Hapus" pada salah satu buku
    • Verifikasi:
      • Confirmation dialog muncul
      • Jika "OK": Buku terhapus, redirect ke index, flash message muncul
      • Jika "Cancel": Buku tidak terhapus
  2. Test dari Detail Page:

    • Buka detail buku
    • Klik button "Hapus Buku" di sidebar
    • Verifikasi sama seperti di atas
  3. Test delete non-existent ID:

    • Manual akses: http://localhost:8000/buku/999 (ID yang tidak ada)
    • Submit delete form
    • Verifikasi: Error 404 atau error message

e. Verifikasi di Database

php artisan tinker
>>> \App\Models\Buku::count() // Hitung total buku
>>> \App\Models\Buku::onlyTrashed()->get() // Jika pakai soft delete

6. PRAKTIKUM 5: Improve UI/UX CRUD

Tujuan

Memperbaiki user experience dengan konfirmasi, loading states, dan auto-hide alerts.

Langkah-langkah

a. SweetAlert2 untuk Konfirmasi Delete

Update layout dengan SweetAlert2:

File: resources/views/layouts/app.blade.php

Tambahkan di <head>:

{{-- SweetAlert2 CSS --}}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.10.0/dist/sweetalert2.min.css">

Tambahkan sebelum closing </body>:

{{-- SweetAlert2 JS --}}
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.10.0/dist/sweetalert2.all.min.js"></script>

b. Update Delete Form dengan SweetAlert

File: resources/views/buku/index.blade.php dan show.blade.php

{{-- Delete Button dengan SweetAlert --}}
<form action="{{ route('buku.destroy', $buku->id) }}" 
      method="POST" 
      class="d-inline delete-form">
    @csrf
    @method('DELETE')
    <button type="button" class="btn btn-sm btn-danger w-100 btn-delete" 
            data-judul="{{ $buku->judul }}">
        <i class="bi bi-trash"></i> Hapus
    </button>
</form>
 
@push('scripts')
<script>
    // SweetAlert confirmation untuk delete
    document.querySelectorAll('.btn-delete').forEach(button => {
        button.addEventListener('click', function(e) {
            e.preventDefault();
            const form = this.closest('form');
            const judul = this.getAttribute('data-judul');
            
            Swal.fire({
                title: 'Konfirmasi Hapus',
                text: `Apakah Anda yakin ingin menghapus buku "${judul}"?`,
                icon: 'warning',
                showCancelButton: true,
                confirmButtonColor: '#d33',
                cancelButtonColor: '#3085d6',
                confirmButtonText: 'Ya, Hapus!',
                cancelButtonText: 'Batal'
            }).then((result) => {
                if (result.isConfirmed) {
                    form.submit();
                }
            });
        });
    });
</script>
@endpush

c. Loading State untuk Form Submit

@push('scripts')
<script>
    // Loading state saat submit form
    document.querySelectorAll('form').forEach(form => {
        form.addEventListener('submit', function() {
            const submitBtn = this.querySelector('button[type="submit"]');
            if (submitBtn && !this.classList.contains('delete-form')) {
                submitBtn.disabled = true;
                submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Menyimpan...';
            }
        });
    });
</script>
@endpush

d. Auto-hide Flash Messages

File: resources/views/layouts/app.blade.php

@if (session('success') || session('error') || session('info') || session('warning'))
    @push('scripts')
    <script>
        // Auto hide alerts after 5 seconds
        setTimeout(function() {
            let alerts = document.querySelectorAll('.alert');
            alerts.forEach(function(alert) {
                let bsAlert = new bootstrap.Alert(alert);
                bsAlert.close();
            });
        }, 5000);
    </script>
    @endpush
@endif

e. Test Improvements

  1. Test delete dengan SweetAlert - dialog muncul
  2. Test submit form - button berubah jadi "Menyimpan..."
  3. Test flash message - otomatis hilang setelah 5 detik

E. TUGAS

Tugas 1: Validation Rules Advanced (30%)

Instruksi: Tambahkan validation rules advanced untuk field-field tertentu.

Spesifikasi:

  1. Custom Validation Rule untuk Kode Buku:
    • Format: BK-[kategori singkat]-[nomor]
    • Contoh: BK-PROG-001, BK-DB-002
    • Buat custom validation rule:
php artisan make:rule KodeBukuFormat
public function passes($attribute, $value)
{
    return preg_match('/^BK-[A-Z]{2,4}-\d{3}$/', $value);
}
 
public function message()
{
    return 'Format kode buku harus: BK-XXX-000 (contoh: BK-PROG-001)';
}
  1. Conditional Validation:

    • Jika kategori "Programming", field bahasa harus "Inggris"
    • Jika tahun terbit < 2000, stok maksimal 5
  2. Custom Error Messages Indonesia:

    • Semua error message harus dalam bahasa Indonesia yang baik

Tugas 2: Bulk Delete Operations (35%)

Instruksi: Implementasi fitur delete multiple buku sekaligus.

Spesifikasi:

  1. Checkbox di Index Page:
<input type="checkbox" name="buku_ids[]" value="{{ $buku->id }}">
  1. Select All Checkbox:
document.getElementById('select-all').addEventListener('change', function() {
    document.querySelectorAll('input[name="buku_ids[]"]').forEach(cb => {
        cb.checked = this.checked;
    });
});
  1. Controller Method:
public function bulkDelete(Request $request)
{
    $ids = $request->buku_ids;
    Buku::whereIn('id', $ids)->delete();
    return redirect()->route('buku.index')
                     ->with('success', count($ids) . ' buku berhasil dihapus!');
}
  1. Route:
Route::post('/buku/bulk-delete', [BukuController::class, 'bulkDelete'])
     ->name('buku.bulk-delete');

Tugas 3: Export Buku ke CSV (35%)

Instruksi: Tambahkan fitur export data buku ke file CSV.

Spesifikasi:

  1. Button Export di Index:
<a href="{{ route('buku.export') }}" class="btn btn-success">
    <i class="bi bi-download"></i> Export CSV
</a>
  1. Controller Method:
public function export()
{
    $bukus = Buku::all();
    
    $filename = 'buku_' . date('Y-m-d_His') . '.csv';
    $headers = [
        'Content-Type' => 'text/csv',
        'Content-Disposition' => 'attachment; filename="' . $filename . '"',
    ];
    
    $callback = function() use ($bukus) {
        $file = fopen('php://output', 'w');
        
        // Header CSV
        fputcsv($file, [
            'Kode Buku', 'Judul', 'Kategori', 'Pengarang', 
            'Penerbit', 'Tahun', 'ISBN', 'Harga', 'Stok'
        ]);
        
        // Data
        foreach ($bukus as $buku) {
            fputcsv($file, [
                $buku->kode_buku,
                $buku->judul,
                $buku->kategori,
                $buku->pengarang,
                $buku->penerbit,
                $buku->tahun_terbit,
                $buku->isbn,
                $buku->harga,
                $buku->stok,
            ]);
        }
        
        fclose($file);
    };
    
    return response()->stream($callback, 200, $headers);
}
  1. Route:
Route::get('/buku/export', [BukuController::class, 'export'])
     ->name('buku.export');

Submission:

  • Format: Link repository GitHub (sertakan screenshot di README)
  • Deadline: Pertemuan 13
  • Upload ke: Ngaji UIN Gusdur (submit link repository GitHub)

F. EVALUASI

1. Kuis Singkat

Pilihan Ganda:

  1. Method HTTP untuk create data adalah:

    • A. GET
    • B. POST
    • C. PUT
    • D. DELETE
  2. Directive Blade untuk CSRF token adalah:

    • A. @token
    • B. @csrf
    • C. @protection
    • D. @security
  3. Method untuk update data dengan mass assignment:

    • A. save()
    • B. update()
    • C. edit()
    • D. modify()
  4. Validation rule untuk email valid:

    • A. 'email' => 'email'
    • B. 'email' => 'valid_email'
    • C. 'email' => 'email_valid'
    • D. 'email' => 'is_email'
  5. Cara passing flash message success:

    • A. ->message('success', 'OK')
    • B. ->flash('success', 'OK')
    • C. ->with('success', 'OK')
    • D. ->session('success', 'OK')
  6. Method spoofing untuk UPDATE menggunakan:

    • A. @method('UPDATE')
    • B. @method('PUT')
    • C. @method('PATCH')
    • D. B dan C benar
  7. Eloquent method untuk delete by ID:

    • A. delete($id)
    • B. remove($id)
    • C. destroy($id)
    • D. erase($id)
  8. Validation rule untuk nilai unik:

    • A. 'field' => 'unique:table'
    • B. 'field' => 'distinct'
    • C. 'field' => 'one'
    • D. 'field' => 'single'
  9. Command untuk generate Form Request:

    • A. php artisan make:form
    • B. php artisan make:request
    • C. php artisan create:request
    • D. php artisan generate:request
  10. Function untuk retrieve old input:

    • A. input('field')
    • B. get('field')
    • C. old('field')
    • D. previous('field')

Essay:

  1. Jelaskan perbedaan create() dan save() untuk insert data! Kapan sebaiknya menggunakan masing-masing? (15 poin)

  2. Apa fungsi CSRF protection? Bagaimana implementasinya di Laravel? (15 poin)

  3. Buatlah validation rules untuk field "email" yang wajib diisi, harus format email valid, dan harus unik di tabel users! (10 poin)

  4. Jelaskan alur lengkap update data dari form hingga database! (15 poin)

  5. Apa keuntungan menggunakan Form Request dibanding validasi di controller? (10 poin)


2. Checklist Kompetensi

NoKompetensiBelumCukupMahir
1Membuat form input○○○
2Implementasi validation○○○
3Create data (store)○○○
4Read data (index, show)○○○
5Update data (edit, update)○○○
6Delete data (destroy)○○○
7CSRF protection○○○
8Flash messages○○○
9Error handling○○○
10Form Request usage○○○

G. REFERENSI

1. Dokumentasi Laravel 12

2. Tools


H. PERSIAPAN PERTEMUAN 13

Topik: CRUD Anggota dengan Laravel

Preview:

  1. Replikasi pola CRUD untuk Anggota
  2. Form dengan date picker
  3. Validation untuk data personal
  4. DRY principle implementation

Yang Perlu Disiapkan:

  1. CRUD Buku sudah berjalan sempurna
  2. Pahami Form Request pattern
  3. Siap refactor code untuk reusability

Pre-reading:

  • DRY Principle
  • Code Refactoring Best Practices

Selamat Belajar! 🚀📝

End of Module - Pertemuan 12

Next: Pertemuan 13 - CRUD Anggota dengan Laravel