Pemrograman Berorientasi Objek PHP: Membangun Aplikasi CRUD dari Nol

Artikel berikut membahas mengenai Pemrograman Berorientasi Objek PHP: Membangun Aplikasi CRUD dari Nol

SAMPUL & HALAMAN AWAL

Judul: Pemrograman Berorientasi Objek PHP: Membangun Aplikasi CRUD dari Nol

Tujuan Ebook: Siswa mampu memahami konsep PBO dalam PHP dan mengimplementasikannya ke dalam aplikasi CRUD (Create, Read, Update, Delete) yang fungsional dan aman.

Target Pembaca: Pemula yang sudah mengenal dasar PHP dan MySQL.

Struktur Ebook: 12 Bab + Glosarium + Latihan Akhir

BAB 1: DASAR-DASAR PEMROGRAMAN BERORIENTASI OBJEK (PBO)

1.1 Pendahuluan

Apa itu PBO? Pemrograman Berorientasi Objek adalah paradigma pemrograman yang berfokus pada "objek" yang memiliki data (properti) dan perilaku (metode). Bayangkan sebuah mobil: mobil memiliki warna, merek (properti) dan kemampuan berjalan, berhenti (metode).

Perbedaan OOP vs Prosedural:

Aspek Prosedural OOP
Data Terpisah dari fungsi Terbungkus dalam objek
Keamanan Rendah Tinggi (enkapsulasi)
Ulang pakai Sulit Mudah (inheritance)
Perawatan Sulit jika besar Terstruktur

1.2 Class dan Object

  • Class: Cetak biru / template untuk membuat objek
  • Object: Instance nyata dari sebuah class
// Membuat Class
class Siswa {
    // Properti
    public $nama;
    public $kelas;
    
    // Method
    public function perkenalan() {
        return "Halo, nama saya " . $this->nama;
    }
}

// Membuat Object
$siswa1 = new Siswa();
$siswa1->nama = "Budi";
echo $siswa1->perkenalan(); // Output: Halo, nama saya Budi

1.3 Property dan Method

  • Property: Variabel dalam class
  • Method: Fungsi dalam class

1.4 Access Modifier

Modifier Class sendiri Turunan Luar class
public
protected
private

Contoh:

class User {
    public $username;    // Bisa diakses semua
    protected $email;    // Hanya class dan turunan
    private $password;   // Hanya class ini saja
    
    public function setPassword($pass) {
        $this->password = password_hash($pass, PASSWORD_DEFAULT);
    }
}

1.5 Constructor dan Destructor

  • Constructor: Method yang otomatis dijalankan saat objek dibuat
  • Destructor: Method yang dijalankan saat objek dihapus
class Koneksi {
    private $conn;
    
    public function __construct() {
        echo "Koneksi dibuat<br>";
        $this->conn = mysqli_connect("localhost", "root", "", "db");
    }
    
    public function __destruct() {
        echo "Koneksi ditutup<br>";
        mysqli_close($this->conn);
    }
}

1.6 Latihan Bab 1

  1. Buat class Produk dengan properti nama dan harga
  2. Tambahkan method info() yang mengembalikan string deskripsi produk
  3. Buat objek dan tampilkan informasinya

BAB 2: PERSIAPAN LINGKUNGAN DAN DATABASE

2.1 Persiapan Tools

Software yang diperlukan:

  1. XAMPP (Apache + MySQL + PHP) atau Laragon
  2. Text Editor (VS Code, Sublime Text, atau PHPStorm)
  3. Browser (Chrome/Firefox)

Langkah instalasi XAMPP:

  1. Download dari apachefriends.org
  2. Install dengan default settings
  3. Jalankan XAMPP Control Panel
  4. Start Apache dan MySQL

2.2 Struktur Folder Project

crud_oop/
│
├── config/          # File konfigurasi
├── classes/         # File class OOP
├── views/           # Tampilan HTML
├── assets/          # CSS, JS, Gambar
└── index.php        # Entry point

2.3 Membuat Database

Buka phpMyAdmin (http://localhost/phpmyadmin) dan jalankan SQL berikut:

CREATE DATABASE sekolah;

USE sekolah;

CREATE TABLE siswa (
    id INT(11) AUTO_INCREMENT PRIMARY KEY,
    nis VARCHAR(20) NOT NULL UNIQUE,
    nama VARCHAR(100) NOT NULL,
    kelas VARCHAR(10) NOT NULL,
    alamat TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Contoh data awal
INSERT INTO siswa (nis, nama, kelas, alamat) VALUES
('001', 'Budi Santoso', '10 RPL 1', 'Jl. Merdeka No.1'),
('002', 'Ani Wijaya', '10 RPL 1', 'Jl. Sudirman No.2');

2.4 Latihan Bab 2

  1. Install XAMPP dan pastikan Apache & MySQL berjalan
  2. Buat database perpustakaan
  3. Buat tabel buku dengan field: id, judul, pengarang, tahun, stok

BAB 3: MEMBUAT CLASS DATABASE DAN KONEKSI

3.1 Konsep Koneksi Database dengan OOP

Kita akan membuat class khusus untuk menangani koneksi database. Class ini akan reusable untuk seluruh project.

3.2 Membuat Class Database

File: config/Database.php

<?php
class Database {
    // Property dengan access modifier private
    private $host = "localhost";
    private $username = "root";
    private $password = "";
    private $database = "sekolah";
    private $connection;
    
    // Constructor: otomatis menjalankan koneksi
    public function __construct() {
        $this->connect();
    }
    
    // Method untuk koneksi
    private function connect() {
        $this->connection = new mysqli(
            $this->host,
            $this->username,
            $this->password,
            $this->database
        );
        
        // Cek error koneksi
        if ($this->connection->connect_error) {
            die("Koneksi gagal: " . $this->connection->connect_error);
        }
        
        // Set charset ke UTF-8
        $this->connection->set_charset("utf8");
    }
    
    // Method untuk mendapatkan objek koneksi
    public function getConnection() {
        return $this->connection;
    }
    
    // Method untuk menutup koneksi
    public function closeConnection() {
        if ($this->connection) {
            $this->connection->close();
        }
    }
}
?>

3.3 Penjelasan Kode

  • private $host: property hanya bisa diakses dari dalam class
  • __construct(): otomatis memanggil connect() saat objek dibuat
  • getConnection(): mengembalikan koneksi untuk digunakan class lain

3.4 Menguji Koneksi

File: test_koneksi.php

<?php
require_once "config/Database.php";

$db = new Database();
$conn = $db->getConnection();

if ($conn) {
    echo "Koneksi berhasil!";
}

$db->closeConnection();
?>

3.5 Latihan Bab 3

  1. Buat class Database dengan parameter host, user, password, db
  2. Tambahkan method query() untuk menjalankan SQL
  3. Uji coba dengan menampilkan data dari tabel siswa

BAB 4: MEMBUAT MODEL (BASE MODEL DAN ENTITY MODEL)

4.1 Konsep Model dalam MVC

Model bertanggung jawab untuk berinteraksi dengan database. Setiap tabel biasanya memiliki satu model.

4.2 Membuat Base Model (Abstract Class)

File: classes/Model.php

<?php
require_once "config/Database.php";

abstract class Model {
    protected $db;
    protected $table;
    protected $primaryKey = "id";
    
    public function __construct() {
        $database = new Database();
        $this->db = $database->getConnection();
    }
    
    // Mendapatkan semua data
    public function all() {
        $sql = "SELECT * FROM {$this->table}";
        $result = $this->db->query($sql);
        return $result->fetch_all(MYSQLI_ASSOC);
    }
    
    // Mendapatkan data berdasarkan ID
    public function find($id) {
        $sql = "SELECT * FROM {$this->table} WHERE {$this->primaryKey} = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->bind_param("i", $id);
        $stmt->execute();
        $result = $stmt->get_result();
        return $result->fetch_assoc();
    }
    
    // Menyimpan data
    public function create($data) {
        $fields = implode(", ", array_keys($data));
        $placeholders = ":" . implode(", :", array_keys($data));
        
        $sql = "INSERT INTO {$this->table} ({$fields}) VALUES ({$placeholders})";
        $stmt = $this->db->prepare($sql);
        
        // Bind parameter dinamis
        foreach ($data as $key => $value) {
            $stmt->bindValue(":{$key}", $value);
        }
        
        return $stmt->execute();
    }
    
    // Mengupdate data
    public function update($id, $data) {
        $setClause = "";
        foreach ($data as $key => $value) {
            $setClause .= "{$key} = :{$key}, ";
        }
        $setClause = rtrim($setClause, ", ");
        
        $sql = "UPDATE {$this->table} SET {$setClause} WHERE {$this->primaryKey} = :id";
        $stmt = $this->db->prepare($sql);
        
        $data['id'] = $id;
        foreach ($data as $key => $value) {
            $stmt->bindValue(":{$key}", $value);
        }
        
        return $stmt->execute();
    }
    
    // Menghapus data
    public function delete($id) {
        $sql = "DELETE FROM {$this->table} WHERE {$this->primaryKey} = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->bind_param("i", $id);
        return $stmt->execute();
    }
}
?>

4.3 Membuat Model untuk Tabel Siswa

File: classes/SiswaModel.php

<?php
require_once "Model.php";

class SiswaModel extends Model {
    protected $table = "siswa";
    protected $primaryKey = "id";
    
    // Method tambahan khusus siswa
    public function searchByNama($keyword) {
        $sql = "SELECT * FROM {$this->table} WHERE nama LIKE ?";
        $stmt = $this->db->prepare($sql);
        $keyword = "%{$keyword}%";
        $stmt->bind_param("s", $keyword);
        $stmt->execute();
        $result = $stmt->get_result();
        return $result->fetch_all(MYSQLI_ASSOC);
    }
    
    public function getByKelas($kelas) {
        $sql = "SELECT * FROM {$this->table} WHERE kelas = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->bind_param("s", $kelas);
        $stmt->execute();
        return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
    }
}
?>

4.4 Latihan Bab 4

  1. Buat model untuk tabel buku dari bab sebelumnya
  2. Tambahkan method getByPengarang($pengarang)
  3. Uji coba method all() dan find()

BAB 5: MEMBUAT CONTROLLER

5.1 Peran Controller

Controller adalah jembatan antara Model (data) dan View (tampilan). Controller:

  • Menerima request dari user
  • Memanggil method yang sesuai dari Model
  • Menyiapkan data untuk View
  • Mengarahkan (redirect) ke halaman yang sesuai

5.2 Membuat Controller Dasar

File: controllers/SiswaController.php

<?php
require_once "classes/SiswaModel.php";

class SiswaController {
    private $siswaModel;
    
    public function __construct() {
        $this->siswaModel = new SiswaModel();
    }
    
    // Menampilkan semua data siswa
    public function index() {
        $siswa = $this->siswaModel->all();
        // Load view dan kirim data
        include "views/siswa/index.php";
    }
    
    // Menampilkan form tambah data
    public function create() {
        include "views/siswa/create.php";
    }
    
    // Menyimpan data baru
    public function store() {
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $data = [
                'nis' => $_POST['nis'],
                'nama' => $_POST['nama'],
                'kelas' => $_POST['kelas'],
                'alamat' => $_POST['alamat']
            ];
            
            if ($this->siswaModel->create($data)) {
                header("Location: index.php?action=index&success=Data berhasil ditambahkan");
            } else {
                header("Location: index.php?action=create&error=Gagal menambahkan data");
            }
        }
    }
    
    // Menampilkan form edit
    public function edit($id) {
        $siswa = $this->siswaModel->find($id);
        include "views/siswa/edit.php";
    }
    
    // Memproses update data
    public function update($id) {
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $data = [
                'nis' => $_POST['nis'],
                'nama' => $_POST['nama'],
                'kelas' => $_POST['kelas'],
                'alamat' => $_POST['alamat']
            ];
            
            if ($this->siswaModel->update($id, $data)) {
                header("Location: index.php?action=index&success=Data berhasil diupdate");
            } else {
                header("Location: index.php?action=edit&id={$id}&error=Gagal mengupdate");
            }
        }
    }
    
    // Menghapus data
    public function destroy($id) {
        if ($this->siswaModel->delete($id)) {
            header("Location: index.php?action=index&success=Data berhasil dihapus");
        } else {
            header("Location: index.php?action=index&error=Gagal menghapus");
        }
    }
}
?>

5.3 Membuat Router Sederhana

File: index.php (Entry Point)

<?php
require_once "controllers/SiswaController.php";

$controller = new SiswaController();

// Mendapatkan action dari URL
$action = isset($_GET['action']) ? $_GET['action'] : 'index';
$id = isset($_GET['id']) ? $_GET['id'] : null;

// Routing
switch ($action) {
    case 'index':
        $controller->index();
        break;
    case 'create':
        $controller->create();
        break;
    case 'store':
        $controller->store();
        break;
    case 'edit':
        $controller->edit($id);
        break;
    case 'update':
        $controller->update($id);
        break;
    case 'destroy':
        $controller->destroy($id);
        break;
    default:
        $controller->index();
        break;
}
?>

5.4 Latihan Bab 5

  1. Buat controller untuk model Buku
  2. Buat router untuk mengakses semua action
  3. Uji coba semua fungsi CRUD melalui URL

BAB 6: MEMBUAT VIEW (TAMPILAN)

6.1 Template Dasar

File: views/templates/header.php

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Aplikasi CRUD Siswa</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .sidebar { min-height: 100vh; background-color: #f8f9fa; }
        .content { padding: 20px; }
        .alert { margin-top: 20px; }
    </style>
</head>
<body>
    <div class="container-fluid">
        <div class="row">
            <div class="col-md-3 sidebar p-3">
                <h4>Menu Utama</h4>
                <ul class="nav flex-column">
                    <li class="nav-item">
                        <a class="nav-link" href="index.php?action=index">Dashboard</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="index.php?action=create">Tambah Siswa</a>
                    </li>
                </ul>
            </div>
            <div class="col-md-9 content">

File: views/templates/footer.php

            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    <script>
        function confirmDelete(id) {
            if (confirm('Apakah Anda yakin ingin menghapus data ini?')) {
                window.location.href = 'index.php?action=destroy&id=' + id;
            }
        }
    </script>
</body>
</html>

6.2 View untuk Menampilkan Data (READ)

File: views/siswa/index.php

<?php include "views/templates/header.php"; ?>

<h2>Data Siswa</h2>

<?php if (isset($_GET['success'])): ?>
    <div class="alert alert-success"><?= htmlspecialchars($_GET['success']) ?></div>
<?php endif; ?>

<?php if (isset($_GET['error'])): ?>
    <div class="alert alert-danger"><?= htmlspecialchars($_GET['error']) ?></div>
<?php endif; ?>

<a href="index.php?action=create" class="btn btn-primary mb-3">Tambah Siswa</a>

<table class="table table-bordered table-striped">
    <thead>
        <tr>
            <th>ID</th>
            <th>NIS</th>
            <th>Nama</th>
            <th>Kelas</th>
            <th>Alamat</th>
            <th>Aksi</th>
        </tr>
    </thead>
    <tbody>
        <?php foreach ($siswa as $row): ?>
        <tr>
            <td><?= $row['id'] ?></td>
            <td><?= htmlspecialchars($row['nis']) ?></td>
            <td><?= htmlspecialchars($row['nama']) ?></td>
            <td><?= htmlspecialchars($row['kelas']) ?></td>
            <td><?= htmlspecialchars($row['alamat']) ?></td>
            <td>
                <a href="index.php?action=edit&id=<?= $row['id'] ?>" class="btn btn-warning btn-sm">Edit</a>
                <button onclick="confirmDelete(<?= $row['id'] ?>)" class="btn btn-danger btn-sm">Hapus</button>
            </td>
        </tr>
        <?php endforeach; ?>
    </tbody>
</table>

<?php include "views/templates/footer.php"; ?>

6.3 View untuk Tambah Data (CREATE)

File: views/siswa/create.php

<?php include "views/templates/header.php"; ?>

<h2>Tambah Data Siswa</h2>

<?php if (isset($_GET['error'])): ?>
    <div class="alert alert-danger"><?= htmlspecialchars($_GET['error']) ?></div>
<?php endif; ?>

<form method="POST" action="index.php?action=store">
    <div class="mb-3">
        <label for="nis" class="form-label">NIS</label>
        <input type="text" class="form-control" id="nis" name="nis" required>
    </div>
    
    <div class="mb-3">
        <label for="nama" class="form-label">Nama Lengkap</label>
        <input type="text" class="form-control" id="nama" name="nama" required>
    </div>
    
    <div class="mb-3">
        <label for="kelas" class="form-label">Kelas</label>
        <select class="form-control" id="kelas" name="kelas" required>
            <option value="">Pilih Kelas</option>
            <option value="10 RPL 1">10 RPL 1</option>
            <option value="10 RPL 2">10 RPL 2</option>
            <option value="11 RPL 1">11 RPL 1</option>
            <option value="11 RPL 2">11 RPL 2</option>
        </select>
    </div>
    
    <div class="mb-3">
        <label for="alamat" class="form-label">Alamat</label>
        <textarea class="form-control" id="alamat" name="alamat" rows="3"></textarea>
    </div>
    
    <button type="submit" class="btn btn-primary">Simpan</button>
    <a href="index.php?action=index" class="btn btn-secondary">Batal</a>
</form>

<?php include "views/templates/footer.php"; ?>

6.4 View untuk Edit Data (UPDATE)

File: views/siswa/edit.php

<?php include "views/templates/header.php"; ?>

<h2>Edit Data Siswa</h2>

<?php if (isset($_GET['error'])): ?>
    <div class="alert alert-danger"><?= htmlspecialchars($_GET['error']) ?></div>
<?php endif; ?>

<form method="POST" action="index.php?action=update&id=<?= $siswa['id'] ?>">
    <div class="mb-3">
        <label for="nis" class="form-label">NIS</label>
        <input type="text" class="form-control" id="nis" name="nis" 
               value="<?= htmlspecialchars($siswa['nis']) ?>" required>
    </div>
    
    <div class="mb-3">
        <label for="nama" class="form-label">Nama Lengkap</label>
        <input type="text" class="form-control" id="nama" name="nama" 
               value="<?= htmlspecialchars($siswa['nama']) ?>" required>
    </div>
    
    <div class="mb-3">
        <label for="kelas" class="form-label">Kelas</label>
        <select class="form-control" id="kelas" name="kelas" required>
            <option value="10 RPL 1" <?= $siswa['kelas'] == '10 RPL 1' ? 'selected' : '' ?>>10 RPL 1</option>
            <option value="10 RPL 2" <?= $siswa['kelas'] == '10 RPL 2' ? 'selected' : '' ?>>10 RPL 2</option>
            <option value="11 RPL 1" <?= $siswa['kelas'] == '11 RPL 1' ? 'selected' : '' ?>>11 RPL 1</option>
            <option value="11 RPL 2" <?= $siswa['kelas'] == '11 RPL 2' ? 'selected' : '' ?>>11 RPL 2</option>
        </select>
    </div>
    
    <div class="mb-3">
        <label for="alamat" class="form-label">Alamat</label>
        <textarea class="form-control" id="alamat" name="alamat" rows="3"><?= htmlspecialchars($siswa['alamat']) ?></textarea>
    </div>
    
    <button type="submit" class="btn btn-primary">Update</button>
    <a href="index.php?action=index" class="btn btn-secondary">Batal</a>
</form>

<?php include "views/templates/footer.php"; ?>

6.5 Latihan Bab 6

  1. Buat tampilan untuk CRUD Buku dengan Bootstrap
  2. Tambahkan fitur search pada halaman index
  3. Buat tampilan detail data (show)

BAB 7: FITUR SEARCH DAN PAGINATION

7.1 Membuat Fitur Pencarian

Tambahkan method di SiswaModel.php:

public function search($keyword) {
    $sql = "SELECT * FROM {$this->table} WHERE nama LIKE ? OR nis LIKE ? OR kelas LIKE ?";
    $stmt = $this->db->prepare($sql);
    $keyword = "%{$keyword}%";
    $stmt->bind_param("sss", $keyword, $keyword, $keyword);
    $stmt->execute();
    $result = $stmt->get_result();
    return $result->fetch_all(MYSQLI_ASSOC);
}

Tambahkan method di SiswaController.php:

public function search() {
    $keyword = isset($_GET['keyword']) ? $_GET['keyword'] : '';
    $siswa = $this->siswaModel->search($keyword);
    include "views/siswa/index.php";
}

Modifikasi form pencarian di views/siswa/index.php:

<form method="GET" class="mb-3">
    <div class="input-group">
        <input type="hidden" name="action" value="search">
        <input type="text" name="keyword" class="form-control" 
               placeholder="Cari nama, NIS, atau kelas..." 
               value="<?= isset($_GET['keyword']) ? htmlspecialchars($_GET['keyword']) : '' ?>">
        <button type="submit" class="btn btn-primary">Cari</button>
    </div>
</form>

7.2 Membuat Pagination

Tambahkan method di SiswaModel.php:

public function paginate($page = 1, $perPage = 5) {
    $offset = ($page - 1) * $perPage;
    $sql = "SELECT * FROM {$this->table} LIMIT {$offset}, {$perPage}";
    $result = $this->db->query($sql);
    $data = $result->fetch_all(MYSQLI_ASSOC);
    
    // Hitung total data
    $totalSql = "SELECT COUNT(*) as total FROM {$this->table}";
    $totalResult = $this->db->query($totalSql);
    $total = $totalResult->fetch_assoc()['total'];
    
    return [
        'data' => $data,
        'total' => $total,
        'page' => $page,
        'perPage' => $perPage,
        'totalPages' => ceil($total / $perPage)
    ];
}

Modifikasi method index di controller:

public function index() {
    $page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
    $pagination = $this->siswaModel->paginate($page, 5);
    $siswa = $pagination['data'];
    $totalPages = $pagination['totalPages'];
    $currentPage = $pagination['page'];
    include "views/siswa/index.php";
}

Tambahkan navigasi pagination di view:

<!-- Setelah tabel, tambahkan: -->
<nav aria-label="Page navigation">
    <ul class="pagination">
        <?php if ($currentPage > 1): ?>
            <li class="page-item">
                <a class="page-link" href="?action=index&page=<?= $currentPage - 1 ?>">Previous</a>
            </li>
        <?php endif; ?>
        
        <?php for ($i = 1; $i <= $totalPages; $i++): ?>
            <li class="page-item <?= $i == $currentPage ? 'active' : '' ?>">
                <a class="page-link" href="?action=index&page=<?= $i ?>"><?= $i ?></a>
            </li>
        <?php endfor; ?>
        
        <?php if ($currentPage < $totalPages): ?>
            <li class="page-item">
                <a class="page-link" href="?action=index&page=<?= $currentPage + 1 ?>">Next</a>
            </li>
        <?php endif; ?>
    </ul>
</nav>

7.3 Latihan Bab 7

  1. Implementasikan fitur search pada aplikasi Buku
  2. Implementasikan pagination
  3. Buat fitur filter berdasarkan kategori

BAB 8: KEAMANAN APLIKASI

8.1 SQL Injection Prevention

Jangan pernah lakukan ini:

// BERBAHAYA! Rentan SQL Injection
$sql = "SELECT * FROM users WHERE username = '$_POST[username]'";

Lakukan ini (Prepared Statement):

$sql = "SELECT * FROM users WHERE username = ?";
$stmt = $this->db->prepare($sql);
$stmt->bind_param("s", $_POST['username']);

8.2 XSS Prevention (Cross Site Scripting)

Gunakan htmlspecialchars() saat menampilkan data user:

// Aman
<?= htmlspecialchars($row['nama']) ?>

// Tidak aman (rentan XSS)
<?= $row['nama'] ?>

8.3 Validasi Input

Buat trait atau method validasi:

trait Validator {
    public function validate($data, $rules) {
        $errors = [];
        
        foreach ($rules as $field => $ruleList) {
            $rules_array = explode('|', $ruleList);
            
            foreach ($rules_array as $rule) {
                if ($rule == 'required' && empty($data[$field])) {
                    $errors[$field][] = ucfirst($field) . " wajib diisi";
                }
                
                if (strpos($rule, 'min:') === 0) {
                    $min = explode(':', $rule)[1];
                    if (strlen($data[$field]) < $min) {
                        $errors[$field][] = ucfirst($field) . " minimal $min karakter";
                    }
                }
                
                if ($rule == 'email' && !filter_var($data[$field], FILTER_VALIDATE_EMAIL)) {
                    $errors[$field][] = ucfirst($field) . " harus format email";
                }
            }
        }
        
        return $errors;
    }
}

8.4 CSRF Protection

Generate token:

class Security {
    public static function generateCSRFToken() {
        if (!isset($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }
    
    public static function verifyCSRFToken($token) {
        if (!isset($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) {
            die("CSRF token tidak valid");
        }
        return true;
    }
}

8.5 Latihan Bab 8

  1. Implementasikan prepared statement di semua query
  2. Tambahkan validasi untuk form tambah dan edit
  3. Implementasikan CSRF protection

BAB 9: SESSION DAN FLASH MESSAGE

9.1 Mengelola Session

File: classes/Session.php

<?php
class Session {
    public static function start() {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
    }
    
    public static function set($key, $value) {
        $_SESSION[$key] = $value;
    }
    
    public static function get($key) {
        return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
    }
    
    public static function has($key) {
        return isset($_SESSION[$key]);
    }
    
    public static function remove($key) {
        if (isset($_SESSION[$key])) {
            unset($_SESSION[$key]);
        }
    }
    
    public static function destroy() {
        session_destroy();
        $_SESSION = [];
    }
    
    // Flash message: pesan yang hanya muncul sekali
    public static function flash($key, $message = null) {
        if ($message) {
            $_SESSION['flash_' . $key] = $message;
        } else {
            $flash = isset($_SESSION['flash_' . $key]) ? $_SESSION['flash_' . $key] : null;
            unset($_SESSION['flash_' . $key]);
            return $flash;
        }
    }
}
?>

9.2 Menggunakan Flash Message di Controller

// Di controller
Session::start();
Session::flash('success', 'Data siswa berhasil ditambahkan');
header("Location: index.php?action=index");

9.3 Menampilkan Flash Message di View

<!-- Di header.php atau index.php -->
<?php 
Session::start();
$success = Session::flash('success');
$error = Session::flash('error');

if ($success): ?>
    <div class="alert alert-success alert-dismissible fade show" role="alert">
        <?= htmlspecialchars($success) ?>
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
    </div>
<?php endif; ?>

<?php if ($error): ?>
    <div class="alert alert-danger alert-dismissible fade show" role="alert">
        <?= htmlspecialchars($error) ?>
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
    </div>
<?php endif; ?>

9.4 Latihan Bab 9

  1. Implementasikan session untuk menyimpan data user login
  2. Gunakan flash message untuk semua notifikasi CRUD
  3. Buat halaman login sederhana

BAB 10: SISTEM AUTENTIKASI (LOGIN/REGISTER)

10.1 Membuat Tabel Users

CREATE TABLE users (
    id INT(11) AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    role ENUM('admin', 'user') DEFAULT 'user',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

10.2 Membuat User Model

File: classes/UserModel.php

<?php
require_once "Model.php";

class UserModel extends Model {
    protected $table = "users";
    
    public function register($username, $email, $password) {
        // Cek apakah username atau email sudah ada
        $checkSql = "SELECT * FROM {$this->table} WHERE username = ? OR email = ?";
        $checkStmt = $this->db->prepare($checkSql);
        $checkStmt->bind_param("ss", $username, $email);
        $checkStmt->execute();
        $result = $checkStmt->get_result();
        
        if ($result->num_rows > 0) {
            return false; // Username atau email sudah terdaftar
        }
        
        // Hash password
        $hashedPassword = password_hash($password, PASSWORD_DEFAULT);
        
        // Insert user baru
        $sql = "INSERT INTO {$this->table} (username, email, password) VALUES (?, ?, ?)";
        $stmt = $this->db->prepare($sql);
        $stmt->bind_param("sss", $username, $email, $hashedPassword);
        
        return $stmt->execute();
    }
    
    public function login($username, $password) {
        $sql = "SELECT * FROM {$this->table} WHERE username = ? OR email = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->bind_param("ss", $username, $username);
        $stmt->execute();
        $result = $stmt->get_result();
        
        if ($result->num_rows === 1) {
            $user = $result->fetch_assoc();
            if (password_verify($password, $user['password'])) {
                unset($user['password']); // Hapus password dari session
                return $user;
            }
        }
        
        return false;
    }
}
?>

10.3 Membuat Auth Controller

File: controllers/AuthController.php

<?php
require_once "classes/UserModel.php";
require_once "classes/Session.php";

class AuthController {
    private $userModel;
    
    public function __construct() {
        $this->userModel = new UserModel();
        Session::start();
    }
    
    public function showLogin() {
        include "views/auth/login.php";
    }
    
    public function login() {
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $username = $_POST['username'];
            $password = $_POST['password'];
            
            $user = $this->userModel->login($username, $password);
            
            if ($user) {
                Session::set('user', $user);
                Session::flash('success', 'Selamat datang, ' . $user['username']);
                header("Location: index.php?action=dashboard");
            } else {
                Session::flash('error', 'Username atau password salah');
                header("Location: index.php?action=login");
            }
        }
    }
    
    public function showRegister() {
        include "views/auth/register.php";
    }
    
    public function register() {
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $username = $_POST['username'];
            $email = $_POST['email'];
            $password = $_POST['password'];
            $confirmPassword = $_POST['confirm_password'];
            
            // Validasi password match
            if ($password !== $confirmPassword) {
                Session::flash('error', 'Password tidak cocok');
                header("Location: index.php?action=register");
                return;
            }
            
            // Validasi panjang password
            if (strlen($password) < 6) {
                Session::flash('error', 'Password minimal 6 karakter');
                header("Location: index.php?action=register");
                return;
            }
            
            if ($this->userModel->register($username, $email, $password)) {
                Session::flash('success', 'Registrasi berhasil! Silakan login');
                header("Location: index.php?action=login");
            } else {
                Session::flash('error', 'Username atau email sudah terdaftar');
                header("Location: index.php?action=register");
            }
        }
    }
    
    public function logout() {
        Session::destroy();
        header("Location: index.php?action=login");
    }
    
    // Middleware untuk cek login
    public static function checkAuth() {
        Session::start();
        if (!Session::has('user')) {
            header("Location: index.php?action=login");
            exit();
        }
    }
    
    // Middleware untuk cek role admin
    public static function checkAdmin() {
        self::checkAuth();
        $user = Session::get('user');
        if ($user['role'] !== 'admin') {
            header("Location: index.php?action=dashboard");
            exit();
        }
    }
}
?>

10.4 View Login dan Register

File: views/auth/login.php

<?php Session::start(); ?>
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <title>Login - Aplikasi CRUD</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
    <div class="container mt-5">
        <div class="row justify-content-center">
            <div class="col-md-4">
                <div class="card">
                    <div class="card-header">
                        <h4 class="text-center">Login</h4>
                    </div>
                    <div class="card-body">
                        <?php $error = Session::flash('error'); ?>
                        <?php if ($error): ?>
                            <div class="alert alert-danger"><?= $error ?></div>
                        <?php endif; ?>
                        
                        <form method="POST" action="index.php?action=doLogin">
                            <div class="mb-3">
                                <label>Username atau Email</label>
                                <input type="text" name="username" class="form-control" required>
                            </div>
                            <div class="mb-3">
                                <label>Password</label>
                                <input type="password" name="password" class="form-control" required>
                            </div>
                            <button type="submit" class="btn btn-primary w-100">Login</button>
                        </form>
                        <div class="text-center mt-3">
                            <a href="index.php?action=register">Belum punya akun? Daftar</a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

10.5 Melindungi Route

Modifikasi index.php router:

<?php
require_once "controllers/SiswaController.php";
require_once "controllers/AuthController.php";
require_once "classes/Session.php";

Session::start();
$action = isset($_GET['action']) ? $_GET['action'] : 'login';

// Route publik (tanpa login)
$publicRoutes = ['login', 'doLogin', 'register', 'doRegister'];

if (!in_array($action, $publicRoutes) && !Session::has('user')) {
    header("Location: index.php?action=login");
    exit();
}

switch ($action) {
    case 'login':
        $controller = new AuthController();
        $controller->showLogin();
        break;
    case 'doLogin':
        $controller = new AuthController();
        $controller->login();
        break;
    case 'register':
        $controller = new AuthController();
        $controller->showRegister();
        break;
    case 'doRegister':
        $controller = new AuthController();
        $controller->register();
        break;
    case 'logout':
        $controller = new AuthController();
        $controller->logout();
        break;
    case 'dashboard':
        include "views/dashboard.php";
        break;
    // Route CRUD siswa (perlu login)
    case 'index':
    case 'create':
    case 'store':
    case 'edit':
    case 'update':
    case 'destroy':
    case 'search':
        $controller = new SiswaController();
        $action($controller);
        break;
    default:
        header("Location: index.php?action=dashboard");
        break;
}
?>

10.6 Latihan Bab 10

  1. Buat sistem registrasi dan login lengkap
  2. Implementasikan middleware untuk melindungi halaman CRUD
  3. Buat fitur "Remember Me" dengan cookie

BAB 11: UPLOAD FILE

11.1 Membuat Tabel dengan Kolom Gambar

ALTER TABLE siswa ADD COLUMN foto VARCHAR(255) DEFAULT NULL;

11.2 Membuat Class Upload Handler

File: classes/Upload.php

<?php
class Upload {
    private $allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/gif'];
    private $maxSize = 2097152; // 2MB
    private $uploadDir;
    
    public function __construct($uploadDir = "uploads/") {
        $this->uploadDir = $uploadDir;
        if (!file_exists($this->uploadDir)) {
            mkdir($this->uploadDir, 0777, true);
        }
    }
    
    public function upload($file, $oldFile = null) {
        // Cek error upload
        if ($file['error'] !== UPLOAD_ERR_OK) {
            return ['success' => false, 'message' => 'Gagal upload file'];
        }
        
        // Cek tipe file
        if (!in_array($file['type'], $this->allowedTypes)) {
            return ['success' => false, 'message' => 'Tipe file tidak diizinkan'];
        }
        
        // Cek ukuran file
        if ($file['size'] > $this->maxSize) {
            return ['success' => false, 'message' => 'Ukuran file maksimal 2MB'];
        }
        
        // Generate nama file unik
        $extension = pathinfo($file['name'], PATHINFO_EXTENSION);
        $newFileName = uniqid() . '.' . $extension;
        $destination = $this->uploadDir . $newFileName;
        
        // Upload file
        if (move_uploaded_file($file['tmp_name'], $destination)) {
            // Hapus file lama jika ada
            if ($oldFile && file_exists($this->uploadDir . $oldFile)) {
                unlink($this->uploadDir . $oldFile);
            }
            
            return ['success' => true, 'filename' => $newFileName];
        }
        
        return ['success' => false, 'message' => 'Gagal memindahkan file'];
    }
    
    public function delete($filename) {
        $filePath = $this->uploadDir . $filename;
        if (file_exists($filePath)) {
            return unlink($filePath);
        }
        return false;
    }
}
?>

11.3 Update Model untuk Mendukung Upload

Modifikasi SiswaModel.php:

public function create($data) {
    $sql = "INSERT INTO {$this->table} (nis, nama, kelas, alamat, foto) VALUES (?, ?, ?, ?, ?)";
    $stmt = $this->db->prepare($sql);
    $stmt->bind_param("sssss", 
        $data['nis'], 
        $data['nama'], 
        $data['kelas'], 
        $data['alamat'], 
        $data['foto']
    );
    return $stmt->execute();
}

public function update($id, $data) {
    $sql = "UPDATE {$this->table} SET nis=?, nama=?, kelas=?, alamat=?, foto=? WHERE id=?";
    $stmt = $this->db->prepare($sql);
    $stmt->bind_param("sssssi", 
        $data['nis'], 
        $data['nama'], 
        $data['kelas'], 
        $data['alamat'], 
        $data['foto'],
        $id
    );
    return $stmt->execute();
}

11.4 Update Controller untuk Upload

Modifikasi method store dan update di SiswaController.php:

public function store() {
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        // Handle upload foto
        $foto = null;
        if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
            $upload = new Upload();
            $result = $upload->upload($_FILES['foto']);
            if ($result['success']) {
                $foto = $result['filename'];
            } else {
                Session::flash('error', $result['message']);
                header("Location: index.php?action=create");
                return;
            }
        }
        
        $data = [
            'nis' => $_POST['nis'],
            'nama' => $_POST['nama'],
            'kelas' => $_POST['kelas'],
            'alamat' => $_POST['alamat'],
            'foto' => $foto
        ];
        
        if ($this->siswaModel->create($data)) {
            Session::flash('success', 'Data berhasil ditambahkan');
            header("Location: index.php?action=index");
        } else {
            Session::flash('error', 'Gagal menambahkan data');
            header("Location: index.php?action=create");
        }
    }
}

public function update($id) {
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        // Ambil data lama untuk mendapatkan foto lama
        $oldData = $this->siswaModel->find($id);
        $foto = $oldData['foto'];
        
        // Handle upload foto baru
        if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
            $upload = new Upload();
            $result = $upload->upload($_FILES['foto'], $oldData['foto']);
            if ($result['success']) {
                $foto = $result['filename'];
            }
        }
        
        $data = [
            'nis' => $_POST['nis'],
            'nama' => $_POST['nama'],
            'kelas' => $_POST['kelas'],
            'alamat' => $_POST['alamat'],
            'foto' => $foto
        ];
        
        if ($this->siswaModel->update($id, $data)) {
            Session::flash('success', 'Data berhasil diupdate');
            header("Location: index.php?action=index");
        } else {
            Session::flash('error', 'Gagal mengupdate data');
            header("Location: index.php?action=edit&id={$id}");
        }
    }
}

public function destroy($id) {
    // Ambil foto sebelum delete
    $siswa = $this->siswaModel->find($id);
    
    if ($this->siswaModel->delete($id)) {
        // Hapus file foto jika ada
        if ($siswa['foto']) {
            $upload = new Upload();
            $upload->delete($siswa['foto']);
        }
        Session::flash('success', 'Data berhasil dihapus');
    } else {
        Session::flash('error', 'Gagal menghapus data');
    }
    
    header("Location: index.php?action=index");
}

11.5 Update View dengan Input File

Tambahkan di create.php dan edit.php:

<div class="mb-3">
    <label for="foto" class="form-label">Foto</label>
    <?php if (isset($siswa) && $siswa['foto']): ?>
        <div class="mb-2">
            <img src="uploads/<?= $siswa['foto'] ?>" width="100" class="img-thumbnail">
        </div>
    <?php endif; ?>
    <input type="file" class="form-control" id="foto" name="foto" accept="image/*">
    <small class="text-muted">Format: JPG, PNG, GIF. Maksimal 2MB</small>
</div>

Update form tag (wajib tambahkan enctype):

<form method="POST" action="..." enctype="multipart/form-data">

Tampilkan foto di index.php:

<th>Foto</th>
// ...
<td>
    <?php if ($row['foto']): ?>
        <img src="uploads/<?= $row['foto'] ?>" width="50" height="50" class="rounded-circle">
    <?php else: ?>
        <span class="text-muted">No photo</span>
    <?php endif; ?>
</td>

11.6 Latihan Bab 11

  1. Implementasikan upload foto untuk data siswa
  2. Buat validasi tipe dan ukuran file
  3. Tambahkan fitur preview gambar sebelum upload

BAB 12: FINAL PROJECT - APLIKASI PERPUSTAKAAN

12.1 Deskripsi Project

Buat aplikasi perpustakaan lengkap dengan fitur:

  • Manajemen Buku (CRUD + upload cover)
  • Manajemen Anggota (CRUD + foto)
  • Manajemen Peminjaman
  • Laporan Peminjaman
  • Dashboard dengan statistik

12.2 Struktur Database

-- Tabel buku
CREATE TABLE buku (
    id INT AUTO_INCREMENT PRIMARY KEY,
    kode_buku VARCHAR(20) UNIQUE,
    judul VARCHAR(200),
    pengarang VARCHAR(100),
    penerbit VARCHAR(100),
    tahun INT,
    stok INT DEFAULT 1,
    cover VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tabel anggota (mirip dengan siswa)
CREATE TABLE anggota (
    id INT AUTO_INCREMENT PRIMARY KEY,
    kode_anggota VARCHAR(20) UNIQUE,
    nama VARCHAR(100),
    alamat TEXT,
    no_telp VARCHAR(15),
    foto VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tabel peminjaman
CREATE TABLE peminjaman (
    id INT AUTO_INCREMENT PRIMARY KEY,
    kode_peminjaman VARCHAR(20) UNIQUE,
    buku_id INT,
    anggota_id INT,
    tanggal_pinjam DATE,
    tanggal_kembali DATE,
    tanggal_pengembalian DATE NULL,
    status ENUM('dipinjam', 'dikembalikan', 'terlambat') DEFAULT 'dipinjam',
    denda INT DEFAULT 0,
    FOREIGN KEY (buku_id) REFERENCES buku(id) ON DELETE CASCADE,
    FOREIGN KEY (anggota_id) REFERENCES anggota(id) ON DELETE CASCADE
);

12.3 Fitur yang Harus Ada

  1. CRUD Buku dengan upload cover
  2. CRUD Anggota dengan upload foto
  3. Transaksi Peminjaman
    • Pinjam buku (kurangi stok)
    • Kembalikan buku (tambah stok, hitung denda)
  4. Dashboard
    • Total buku
    • Total anggota
    • Total peminjaman aktif
    • Grafik sederhana
  5. Laporan
    • Laporan peminjaman per periode
    • Export ke Excel
  6. Pencarian dan Filter untuk semua data

12.4 Panduan Pengerjaan

  1. Minggu 1: Setup database dan membuat model untuk Buku, Anggota, Peminjaman
  2. Minggu 2: Membuat controller dan view untuk Buku & Anggota
  3. Minggu 3: Implementasi transaksi peminjaman dan pengembalian
  4. Minggu 4: Dashboard, laporan, dan finishing

12.5 Kriteria Penilaian

Kriteria Bobot
Fungsionalitas CRUD 25%
Implementasi OOP dengan benar 20%
Keamanan (SQL injection, XSS) 15%
User Interface & UX 15%
Fitur tambahan (upload, export) 15%
Dokumentasi kode 10%

GLOSARIUM

Istilah Arti
Class Cetak biru atau template untuk membuat objek
Object Instance nyata dari sebuah class
Property Variabel yang berada dalam class
Method Fungsi yang berada dalam class
Constructor Method yang otomatis dijalankan saat objek dibuat
Inheritance Pewarisan properti dan method dari class parent ke child
Encapsulation Pembungkusan data dan method dalam class
Polymorphism Kemampuan method untuk memiliki banyak bentuk
MVC Model-View-Controller, pola arsitektur aplikasi
CRUD Create, Read, Update, Delete - operasi dasar database
Prepared Statement Teknik untuk mencegah SQL injection
XSS Cross Site Scripting, serangan melalui injeksi script
CSRF Cross Site Request Forgery, serangan pemalsuan request
Session Mekanisme menyimpan data user sementara
Middleware Lapisan antara request dan response untuk validasi

DAFTAR PUSTAKA

  1. PHP Documentation - php.net
  2. MySQL Documentation - mysql.com
  3. Bootstrap Documentation - getbootstrap.com
  4. "Object-Oriented PHP" oleh Peter Lavin (No Starch Press)
  5. "PHP and MySQL Web Development" oleh Luke Welling & Laura Thomson


Selamat Belajar! Praktikkan setiap kode, jangan hanya membaca. Semakin sering Anda mengetik kode, semakin cepat Anda menguasai PBO PHP.