Enum murni pada chapter sebelumnya sempurna untuk logika internal program, tetapi data nyata jarang berbentuk object. Kolom database menyimpan string menunggu, API eksternal mengirim angka 2, dan form HTML mengirim teks mentah. PHP 8.1 menjembatani dua dunia tersebut lewat backed enum: enum yang setiap case-nya ditautkan (backed) dengan satu nilai skalar — int atau string. Pada chapter ini kita pelajari cara mendeklarasikan backed enum, mengonversi bolak-balik antara nilai mentah dan case, lalu menerapkannya pada pola pemetaan kolom database yang lazim dipakai di aplikasi nyata.

Menautkan Nilai Skalar pada Case #

Deklarasi backed enum ditandai tipe skalar setelah nama enum (: string atau : int), dan setiap case wajib diberi nilai melalui tanda sama dengan:

php
<?php

enum Status: string
{
    case Aktif = "aktif";
    case Nonaktif = "nonaktif";
    case Ditangguhkan = "ditangguhkan";
}

$status = Status::Ditangguhkan;

echo "name: {$status->name}\n";
echo "value: {$status->value}\n";
echo "json: " . json_encode($status) . "\n";

Output yang Dihasilkan:

text
name: Ditangguhkan
value: ditangguhkan
json: "ditangguhkan"

Ada dua property read-only yang kini aktif pada setiap case: $name berisi nama case (selalu ada, bahkan di enum murni), sedangkan $value berisi nilai skalar yang kita tautkan. Perhatikan baris terakhir: json_encode() pada backed enum otomatis menghasilkan nilai skalarnya ("ditangguhkan") — enum mengimplementasikan JsonSerializable secara bawaan, sehingga serialisasi ke JSON tidak perlu dikonversi manual. Nilai case harus berupa literal; PHP juga menolak nilai duplikat dalam satu enum agar konversi dari nilai ke case selalu satu arah yang jelas.

Konversi dari Nilai dengan from() #

Arah yang lebih penting adalah sebaliknya: mengubah nilai mentah menjadi case. Method statis from() mengerjakannya:

php
<?php

enum Status: string
{
    case Aktif = "aktif";
    case Nonaktif = "nonaktif";
}

$status = Status::from("aktif");
echo $status->name . "\n";

Output yang Dihasilkan:

text
Aktif

Pemanggilan Status::from("aktif") mencari case bernilai "aktif" dan mengembalikan instance Status::Aktif. Namun from() bersifat ketat — nilai yang tidak terdaftar langsung melempar pengecualian:

php
<?php

enum Status: string
{
    case Aktif = "aktif";
    case Nonaktif = "nonaktif";
}

$status = Status::from("dihapus");

Output yang Dihasilkan:

text

Fatal error: Uncaught ValueError: "dihapus" is not a valid backing value for enum Status in /home/z/my-project/php-course/content/php/advanced-object-model/backed-enums/.verify_tmp.php:9
Stack trace:
#0 /home/z/my-project/php-course/content/php/advanced-object-model/backed-enums/.verify_tmp.php(9): Status::from('dihapus')
#1 {main}
  thrown in /home/z/my-project/php-course/content/php/advanced-object-model/backed-enums/.verify_tmp.php on line 9

ValueError: "dihapus" is not a valid backing value for enum "Status" menghentikan eksekusi karena string tersebut memang bukan bagian dari kontrak enum. Perilaku ini tepat untuk data yang seharusnya selalu sah — misalnya kolom database yang nilai-kolomnya kita kendalikan penuh lewat migrasi. Jika from() gagal di jalur tersebut, berarti ada ketidaksesuaian skema yang perlu diperbaiki, bukan input pengguna yang perlu dimaafkan.

tryFrom() untuk Input yang Belum Pasti Sah #

Untuk data dari sumber yang tidak kita kendalikan — form, query string, payload API pihak ketiga — gunakan tryFrom() yang mengembalikan null alih-alih melempar pengecualian:

php
<?php

enum Status: string
{
    case Aktif = "aktif";
    case Nonaktif = "nonaktif";
}

$valid = Status::tryFrom("aktif");
$tidakValid = Status::tryFrom("dihapus");

var_dump($valid);
var_dump($tidakValid);

Output yang Dihasilkan:

text
enum(Status::Aktif)
NULL

Ringkasan perilaku keduanya:

MethodNilai validNilai tidak terdaftar
Status::from($nilai)mengembalikan casemelempar ValueError
Status::tryFrom($nilai)mengembalikan casemengembalikan null

Enum Bertipe int dan Pencocokan Nilai #

Backing bertipe int cocok untuk skema yang menghemat ruang penyimpanan atau mewarisi kode numerik lama. Setelah nilai diubah menjadi case, seluruh keunggulan enum kembali tersedia — termasuk pencocokan match:

php
<?php

enum Peran: int
{
    case Tamu = 0;
    case Member = 1;
    case Admin = 2;
}

function hakAkses(Peran $peran): string
{
    return match ($peran) {
        Peran::Tamu => "hanya membaca",
        Peran::Member => "baca dan komentar",
        Peran::Admin => "akses penuh",
    };
}

$peranDariDb = 2;
$peran = Peran::from($peranDariDb);

echo "Peran #{$peran->value} ({$peran->name}): " . hakAkses($peran) . "\n";

Output yang Dihasilkan:

text
Peran #2 (Admin): akses penuh

Angka 2 dari kolom database dikonversi sekali oleh Peran::from(), dan setelah itu kode hanya bekerja dengan case — kebalikan dari pola lama yang menyebarkan angka ajaib ke seluruh codebase. Nilai 0 di sini sah: yang terlarang adalah mendeklarasikan case tanpa nilai atau mencampur tipe backing dalam satu enum.

Pola Pemetaan Kolom Database #

Pola paling umum di aplikasi nyata: menyimpan $value di kolom database, lalu mengembalikannya menjadi case saat membaca. Contoh berikut memakai SQLite in-memory sebagai ilustrasi (lihat chapter PDO untuk dasar koneksinya):

php
<?php

enum StatusPesanan: string
{
    case Menunggu = "menunggu";
    case Diproses = "diproses";
    case Selesai = "selesai";

    public function label(): string
    {
        return match ($this) {
            self::Menunggu => "Menunggu pembayaran",
            self::Diproses => "Sedang diproses",
            self::Selesai => "Selesai",
        };
    }
}

$pdo = new PDO("sqlite::memory:");
$pdo->exec("CREATE TABLE pesanan (id INTEGER PRIMARY KEY, status TEXT)");
$pdo->exec("INSERT INTO pesanan (status) VALUES ('menunggu'), ('selesai')");

$rows = $pdo->query("SELECT id, status FROM pesanan ORDER BY id")->fetchAll(PDO::FETCH_ASSOC);

foreach ($rows as $row) {
    $status = StatusPesanan::from($row["status"]);
    echo "Pesanan #{$row["id"]}: " . $status->label() . "\n";
}

Output yang Dihasilkan:

text
Pesanan #1: Menunggu pembayaran
Pesanan #2: Selesai

Kolom status menyimpan string skalar, tetapi begitu baris dibaca, StatusPesanan::from() langsung mengubahnya menjadi case sehingga loop hanya berinteraksi dengan tipe enum. Method label() yang kita definisikan sendiri membuat tampilan antarmuka ikut terpusat di satu tempat. Arah tulis pun sederhana: simpan $status->value (atau biarkan PDO yang mengirim property tersebut) — konversinya simetris dan eksplisit di kedua sisi.

Pola Validasi Input #

tryFrom() berpasangan naturally dengan operator ?? untuk menetapkan nilai default yang aman saat input tidak dikenali:

php
<?php

enum Status: string
{
    case Aktif = "aktif";
    case Nonaktif = "nonaktif";
    case Ditangguhkan = "ditangguhkan";
}

function statusDariInput(string $input): Status
{
    return Status::tryFrom(strtolower(trim($input))) ?? Status::Nonaktif;
}

echo statusDariInput("  AKTIF ")->name . "\n";
echo statusDariInput("tidak-tahu")->name . "\n";

Output yang Dihasilkan:

text
Aktif
Nonaktif

Input " AKTIF " dinormalisasi lebih dulu dengan trim() dan strtolower() sehingga cocok dengan nilai case "aktif"; hasilnya case Aktif dikembalikan. Input asing "tidak-tahu" membuat tryFrom() mengembalikan null, lalu ?? menjatuhkannya ke default Status::Nonaktif — return type function tetap Status, bukan ?Status, karena jalur null sudah tertutup di baris yang sama. Pola satu baris ini menggantikan rangkaian in_array() atau switch validasi manual yang biasa kita tulis di masa lalu.

Ringkasan #

Backed enum melengkapi enum murni dengan jembatan dua arah ke dunia skalar: $status->value saat menyimpan, from() atau tryFrom() saat membaca, dan pemilihan keduanya ditentukan oleh seberapa besar kita mempercayai sumber data. Nilai mentah berhenti menyebar ke logika bisnis pada batas sistem — begitu melewati konversi, seluruh kekuatan sistem tipe enum bekerja penuh. Kontrak “nilai + metadata” semacam ini ternyata bisa diketangkan lebih jauh lagi: PHP mengizinkan kita menempelkan informasi terstruktur pada class, method, dan property lewat attribute, yang kita bahas pada chapter berikutnya.

Referensi #