Class exception bawaan seperti RuntimeException cukup untuk melempar kegagalan, tetapi satu jenis untuk semua keperluan membuat blok catch sulit memilah. Ketika penarikan saldo gagal, koneksi database terputus, dan JSON rusak semuanya berupa RuntimeException, pemanggil tidak bisa memberi respons berbeda untuk tiap kegagalan. Pada chapter ini kita belajar membuat custom exception, yaitu class exception milik kita sendiri, mulai dari class kosong paling sederhana hingga class dengan data tambahan seperti daftar kesalahan validasi.
Mengapa Membutuhkan Jenis Exception Sendiri #
Kekuatan utama exception terletak pada tipenya, bukan pesannya. Blok catch mencocokkan tipe objek yang dilempar; pesan hanyalah teks yang ikut terbawa. Dengan mendefinisikan class sendiri, satu blok catch dapat menangani secara presisi hanya kegagalan yang memang relevan, sementara kegagalan lain tetap naik ke lapisan di atasnya. Mari kita lihat perbandingannya dalam kode:
<?php
class SaldoTidakCukup extends RuntimeException
{
}
function tarikUang(float $saldo, float $jumlah): float
{
if ($jumlah > $saldo) {
throw new SaldoTidakCukup(
"Saldo Rp" . number_format($saldo, 0, ",", ".")
. " tidak cukup untuk penarikan Rp" . number_format($jumlah, 0, ",", ".") . "."
);
}
return $saldo - $jumlah;
}
try {
tarikUang(50000, 150000);
} catch (SaldoTidakCukup $e) {
echo "Transaksi ditolak: " . $e->getMessage() . "\n";
echo "Sisa saldo tidak berubah.\n";
}Output yang Dihasilkan:
Transaksi ditolak: Saldo Rp50.000 tidak cukup untuk penarikan Rp150.000.
Sisa saldo tidak berubah.Perhatikan bahwa class SaldoTidakCukup tidak berisi apa pun selain kurung kurawal kosong. Class kosong tersebut tetap mewarisi seluruh perilaku RuntimeException, namun kini memiliki identitas tersendiri. Jika suatu saat function tarikUang() juga melempar RuntimeException biasa untuk kegagalan lain, blok catch (SaldoTidakCukup $e) tidak akan tersalah panggil, karena pencocokan tipe memang hanya menangkap SaldoTidakCukup dan turunannya.
Anatomi Custom Exception #
Membuat custom exception pada dasarnya adalah pewarisan class biasa: extends Exception atau extends RuntimeException, lalu beri nama yang menggambarkan kegagalan domain. Karena seluruh method inti diwarisi dari Exception, class anak langsung siap dipakai. Method-method warisan yang paling sering dipakai:
| Method warisan | Isi |
|---|---|
getMessage() | Pesan kegagalan saat throw |
getCode() | Kode error numerik (parameter kedua constructor) |
getFile() dan getLine() | Berkas dan baris tempat throw terjadi |
getPrevious() | Exception penyebab dari rantai rethrow |
getTraceAsString() | Jejak call stack dalam bentuk teks |
Konvensi penamaan di ekosistem PHP umumnya memakai akhiran Exception (misalnya SaldoTidakCukupException). Penamaan tanpa akhiran juga lazim di banyak framework modern selama konsisten dalam satu proyek; chapter ini memakai nama tanpa akhiran agar contoh lebih ringkas.
Menambahkan Data pada Exception #
Pesan string saja sering tidak cukup. Pada validasi form, kita biasanya ingin membawa seluruh daftar field yang salah beserta alasannya, agar pemanggil bisa menampilkan semuanya sekaligus. Caranya adalah menimpa (override) constructor dan menambahkan property sendiri:
<?php
class ValidationError extends RuntimeException
{
public function __construct(
private array $errors,
string $message = "Data yang dikirim tidak valid."
) {
parent::__construct($message);
}
public function getErrors(): array
{
return $this->errors;
}
}
function validasiPendaftaran(array $data): void
{
$errors = [];
if (strlen($data["username"] ?? "") < 5) {
$errors["username"] = "Username minimal 5 karakter.";
}
if (!filter_var($data["email"] ?? "", FILTER_VALIDATE_EMAIL)) {
$errors["email"] = "Format email tidak valid.";
}
if ($errors !== []) {
throw new ValidationError($errors);
}
}
try {
validasiPendaftaran(["username" => "budi", "email" => "budi-example"]);
} catch (ValidationError $e) {
echo $e->getMessage() . "\n";
foreach ($e->getErrors() as $field => $pesan) {
echo "- $field: $pesan\n";
}
}Output yang Dihasilkan:
Data yang dikirim tidak valid.
- username: Username minimal 5 karakter.
- email: Format email tidak valid.Mari kita bedah bagian-bagian pentingnya:
- Baris 3-11 (constructor override): constructor baru memakai constructor property promotion untuk menyimpan array
$errorssekaligus. Barisparent::__construct($message)wajib dipanggil agar sisi internal classException(pesan, kode, penelusuran stack) tetap terisi dengan benar. - Baris 13-16 (
getErrors()): method pengambil (getter) sederhana mengekspos data tambahan ke pemanggil. Pemanggil tidak perlu mengurai string pesan untuk mengetahui field mana saja yang salah. - Baris 19-33 (
validasiPendaftaran()): function mengumpulkan semua kesalahan ke dalam$errorsterlebih dahulu, lalu melempar satuValidationErroryang membawa seluruh daftar tersebut. Pola “kumpulkan lalu lempar sekali” lebih ramah pengguna dibanding berhenti di kesalahan pertama. - Baris 36-42 (catch): pemanggil menampilkan pesan utama, lalu mengiterasi
getErrors()untuk mencetak tiap field beserta alasannya.
Memilih Basis yang Tepat dari SPL #
Sebelum membuat class baru, periksa dulu apakah SPL sudah menyediakan jenis yang cocok. SPL (Standard PHP Library) mengirim sejumlah class exception siap pakai yang terbagi dalam dua keluarga besar: LogicException untuk kesalahan pemrograman yang seharusnya bisa dicegah oleh penulis kode, dan RuntimeException untuk kegagalan yang baru terlihat saat program berjalan. Kelima class berikut adalah yang paling sering dipakai:
| Class SPL | Keluarga | Kapan dipakai |
|---|---|---|
InvalidArgumentException | Logic | Argumen function tidak sesuai kontrak |
DomainException | Logic | Nilai melanggar aturan domain bisnis |
BadFunctionCallException | Logic | Function dipanggil dengan cara yang salah |
RuntimeException | Runtime | Kegagalan umum saat eksekusi |
UnexpectedValueException | Runtime | Nilai kembalian tidak sesuai dugaan |
Contoh berikut memperlihatkan pemilihan jenis yang berbeda untuk dua kondisi gagal yang berbeda:
<?php
function hitungPajak(float $penghasilan, float $tarif): float
{
if ($tarif < 0.0 || $tarif > 1.0) {
throw new InvalidArgumentException("Tarif harus di antara 0 dan 1, diberikan: $tarif");
}
if ($penghasilan < 0.0) {
throw new DomainException("Penghasilan tidak boleh negatif.");
}
return $penghasilan * $tarif;
}
try {
echo hitungPajak(10000000, 0.1) . "\n";
hitungPajak(10000000, 1.5);
} catch (InvalidArgumentException | DomainException $e) {
echo get_class($e) . ": " . $e->getMessage() . "\n";
}Output yang Dihasilkan:
1000000
InvalidArgumentException: Tarif harus di antara 0 dan 1, diberikan: 1.5Tarif 1.5 jelas salah ketik atau salah hitung dari sisi pemanggil, sehingga InvalidArgumentException adalah pilihan tepat. Sementara penghasilan negatif secara teknis bertipe benar (float), tetapi melanggar aturan domain perpajakan, sehingga DomainException lebih menggambarkan masalahnya. Blok catch memakai multi-catch InvalidArgumentException | DomainException untuk menangkap keduanya sekaligus, dan get_class() membuktikan bahwa tipenya tetap terjaga meski ditangkap bersama.
Marker Exception untuk Mengelompokkan Kegagalan #
Pola terakhir yang sering dipakai di aplikasi besar adalah marker: sebuah interface kosong yang diimplementasikan banyak class exception. Interface kosong tidak menambah perilaku apa pun, tetapi memberi pemanggil satu tipe umum untuk menangkap sekelompok kegagalan yang secara bisnis saling terkait:
<?php
interface PembayaranGagal
{
}
class KartuDitolak extends RuntimeException implements PembayaranGagal
{
}
class TokenKedaluwarsa extends RuntimeException implements PembayaranGagal
{
}
function bayarDenganKartu(): void
{
throw new KartuDitolak("Kartu kredit ditolak oleh bank.");
}
function bayarDenganToken(): void
{
throw new TokenKedaluwarsa("Token pembayaran sudah kedaluwarsa.");
}
try {
bayarDenganKartu();
} catch (PembayaranGagal $e) {
echo "Tipe: " . get_class($e) . "\n";
echo "Mohon ulangi pembayaran: " . $e->getMessage() . "\n";
}Output yang Dihasilkan:
Tipe: KartuDitolak
Mohon ulangi pembayaran: Kartu kredit ditolak oleh bank.Blok catch (PembayaranGagal $e) menangkap objek apa pun yang mengimplementasikan interface tersebut, termasuk KartuDitolak maupun TokenKedaluwarsa. Handler halaman pembayaran cukup menulis satu blok catch dengan pesan “mohon ulangi pembayaran” untuk semua varian kegagalan, tanpa perlu menghitung satu per satu. Pada saat yang sama, tipe aslinya tetap tersedia melalui get_class() atau instanceof, sehingga lapisan yang lebih dalam masih bisa memilah dengan presisi bila diperlukan.
Ringkasan #
Custom exception dibuat dengan mewarisi Exception atau salah satu class SPL, dan class kosong pun sudah memberi identitas baru yang bisa ditangkap secara spesifik. Constructor bisa ditimpa untuk membawa data tambahan seperti daftar kesalahan validasi, dan interface marker mengelompokkan banyak jenis kegagalan ke dalam satu tipe tangkapan. Keluarga LogicException dan RuntimeException beserta seluruh kerabat SPL ternyata membentuk pohon hierarki yang cukup dalam. Pada chapter berikutnya kita memetakan pohon tersebut secara menyeluruh, dari akar Throwable hingga daun-daunnya.