Section ini telah membekali kita dengan peralatan lengkap: typed dan readonly properties, constructor promotion, property hooks, enum, attribute, reflection, generator, iterator, hingga weak reference. Peralatan yang baik tetap bergantung pada cara pakainya — object model yang kaya justru memudahkan kita membangun struktur yang rumit tanpa sadar. Chapter penutup ini merangkum praktik terbaik yang paling sering membedakan kode yang sehat dari kode yang membusuk seiring bertambahnya fitur. Setiap prinsip disertai contoh mini yang bisa dijalankan, lalu ditutup tabel anti-pola dan daftar periksa singkat.

Utamakan Imutabilitas #

Object yang tidak pernah berubah setelah dibuat lebih mudah dijamin benar: tidak ada metode lain yang bisa diam-diam mengubah isinya, dan object seperti itu aman dibagikan ke mana pun. Kombinasi final, constructor promotion, dan readonly menghasilkan value object yang ringkas:

php
<?php
final class Uang
{
    public function __construct(
        public readonly int $jumlah,
        public readonly string $mataUang = 'IDR',
    ) {
    }

    public function tambah(Uang $lain): static
    {
        if ($lain->mataUang !== $this->mataUang) {
            throw new InvalidArgumentException('Mata uang harus sama');
        }

        return new static($this->jumlah + $lain->jumlah, $this->mataUang);
    }
}

$tabungan = new Uang(50000);
$total = $tabungan->tambah(new Uang(25000));

echo 'Tabungan awal: Rp' . $tabungan->jumlah . "\n";
echo 'Total baru: Rp' . $total->jumlah . "\n";

Output yang Dihasilkan:

text
Tabungan awal: Rp50000
Total baru: Rp75000

Metode tambah() tidak menyentuh $tabungan sama sekali — metode tersebut membangun object Uang baru. Output membuktikan keduanya hidup berdampingan: tabungan awal tetap 50000, sementara total baru bernilai 75000. Penegakan readonly dilakukan oleh mesin PHP sendiri, bukan sekadar kesepakatan santai antar penulis kode:

php
<?php
final class Uang
{
    public function __construct(
        public readonly int $jumlah,
    ) {
    }
}

$tabungan = new Uang(50000);
$tabungan->jumlah = 99999;

Output yang Dihasilkan:

text

Fatal error: Uncaught Error: Cannot modify readonly property Uang::$jumlah in /home/z/my-project/php-course/content/php/advanced-object-model/object-model-best-practices/.verify_tmp.php:11
Stack trace:
#0 {main}
  thrown in /home/z/my-project/php-course/content/php/advanced-object-model/object-model-best-practices/.verify_tmp.php on line 11

Upaya penimpaan berakhir Error sebelum baris berikutnya sempat berjalan — bug mutasi yang biasanya senyap kini gagal keras dan terlokalisasi di satu baris.

Komposisi di Atas Pewarisan #

Pewarisan memangku hubungan “adalah spesialis dari”, dan mudah disalahgunakan sekadar untuk meminjam kode. Komposisi — menyusun object dari object lain — menghasilkan relasi yang lebih longgar dan mudah diganti:

php
<?php
class PenulisPesan
{
    public function tulis(string $untuk, string $isi): string
    {
        return "Kepada $untuk: $isi";
    }
}

class LayananPesanan
{
    public function __construct(private PenulisPesan $penulis)
    {
    }

    public function konfirmasi(string $namaPelanggan): string
    {
        return $this->penulis->tulis($namaPelanggan, 'pesanan Anda sedang diproses');
    }
}

$layanan = new LayananPesanan(new PenulisPesan());
echo $layanan->konfirmasi('Budi') . "\n";

Output yang Dihasilkan:

text
Kepada Budi: pesanan Anda sedang diproses

LayananPesanan tidak mewarisi PenulisPesan; layanan tersebut memiliki seorang penulis pesan yang disuntikkan lewat constructor. Saat kebutuhan berubah — penulis pesan berganti kirim email atau menulis log — cukup kirim object lain dengan metode tulis() yang setara, tanpa membongkar hierarki class. Prinsip praktisnya: gunakan pewarisan hanya untuk variasi yang sungguh-sungguh “adalah”, dan komposisi untuk semua kebutuhan lain.

Antarmuka Kecil dan Spesifik #

Antarmuka yang gemuk memaksa implementasi menyediakan metode yang tidak dibutuhkan. Pecah kontrak besar menjadi beberapa antarmuka kecil, lalu minta kontrak paling kecil yang cukup untuk setiap konsumen:

php
<?php
interface DapatDibaca
{
    public function baca(string $kunci): ?string;
}

interface DapatDitulis
{
    public function tulisData(string $kunci, string $nilai): void;
}

class PenyimpananSederhana implements DapatDibaca, DapatDitulis
{
    private array $data = [];

    public function baca(string $kunci): ?string
    {
        return $this->data[$kunci] ?? null;
    }

    public function tulisData(string $kunci, string $nilai): void
    {
        $this->data[$kunci] = $nilai;
    }
}

class PembacaKonfigurasi
{
    public function __construct(private DapatDibaca $sumber)
    {
    }

    public function ambil(string $kunci): string
    {
        return $this->sumber->baca($kunci) ?? '(tidak diatur)';
    }
}

$penyimpanan = new PenyimpananSederhana();
$penyimpanan->tulisData('nama_aplikasi', 'Belajar PHP');

$konfigurasi = new PembacaKonfigurasi($penyimpanan);
echo $konfigurasi->ambil('nama_aplikasi') . "\n";
echo $konfigurasi->ambil('tema_warna') . "\n";

Output yang Dihasilkan:

text
Belajar PHP
(tidak diatur)

PembacaKonfigurasi hanya butuh membaca, sehingga constructor-nya hanya meminta DapatDibaca — meskipun object yang masuk juga mampu menulis. Class PenyimpananSederhana tetap bebas mengimplementasikan dua antarmuka sekaligus, tetapi setiap konsumen bergantung pada irisan terkecil yang ia perlukan. Konsumen dengan kontrak kecil juga lebih mudah diuji: stub untuk baca() saja jauh lebih ringkas daripada stub penyimpanan penuh.

Hindari State Statis #

Property statis adalah variabel global berbaju class: satu salinan per proses, terlihat dari mana saja, dan bisa diubah siapa saja. Bandingkan dua penghitung berikut:

php
<?php
class PenghitungStatis
{
    public static int $total = 0;

    public static function naik(): void
    {
        self::$total++;
    }
}

class PenghitungInstansi
{
    private int $total = 0;

    public function naik(): int
    {
        return ++$this->total;
    }
}

PenghitungStatis::naik();
PenghitungStatis::naik();
echo 'Statis: ' . PenghitungStatis::$total . "\n";

$keranjangA = new PenghitungInstansi();
$keranjangB = new PenghitungInstansi();
echo 'Keranjang A: ' . $keranjangA->naik() . ' lalu ' . $keranjangA->naik() . "\n";
echo 'Keranjang B: ' . $keranjangB->naik() . "\n";

Output yang Dihasilkan:

text
Statis: 2
Keranjang A: 1 lalu 2
Keranjang B: 1

PenghitungStatis::$total terus bertambah sepanjang hidup proses dan tidak bisa dipisahkan per konteks, sehingga dua keranjang yang seharusnya saling independen malah berbagi angka. PenghitungInstansi memisahkan state per object: keranjang A dan keranjang B masing-masing berhitung dari nol. Gunakan statis hanya untuk konstanta sesungguhnya atau factory murni tanpa state — bukan sebagai jalan pintas antar bagian kode.

Exception, Bukan Kode Error #

Mengembalikan null, false, atau angka error membuat kegagalan mudah diabaikan: pemanggil lupa memeriksa, dan bug melaju senyap. Exception memaksa kegagalan ditangani atau diteruskan dengan jelas:

php
<?php
class PenggunaTidakDitemukan extends RuntimeException
{
}

function cariVersiKode(int $id): ?array
{
    $semua = [1 => ['nama' => 'Budi'], 2 => ['nama' => 'Siti']];
    return $semua[$id] ?? null;
}

function cariVersiException(int $id): array
{
    $semua = [1 => ['nama' => 'Budi'], 2 => ['nama' => 'Siti']];
    if (!isset($semua[$id])) {
        throw new PenggunaTidakDitemukan("Pengguna #$id tidak ada");
    }
    return $semua[$id];
}

$hasil = cariVersiKode(99);
echo $hasil === null ? 'Versi kode: null, pemanggil wajib memeriksa' : 'ditemukan';
echo "\n";

try {
    cariVersiException(99);
} catch (PenggunaTidakDitemukan $e) {
    echo 'Versi exception: ' . $e->getMessage() . "\n";
}

Output yang Dihasilkan:

text
Versi kode: null, pemanggil wajib memeriksa
Versi exception: Pengguna #99 tidak ada

Kedua versi sama-sama gagal menemukan pengguna 99, tetapi konsekuensinya berbeda. Versi kode error bergantung pada disiplin pemanggil untuk memeriksa null — lupa memeriksa berarti bug senyap. Versi exception memutus alur normal dengan PenggunaTidakDitemukan, sehingga pemanggil secara eksplisit memilih: menangkap dan menangani, atau membiarkannya naik ke pemanggil berikutnya. Pesan exception juga membawa konteks (Pengguna #99 tidak ada) yang jauh lebih berbicara daripada false.

final secara Default: Pro dan Kontra #

Banyak tim modern mewajibkan final pada class baru dan hanya membukanya ketika pewarisan benar-benar dirancang. Sisi pro: hierarki yang tidak sengaja terbentuk tercegah, komposisi terdorong, dan perombakan internal class tidak merusak class anak yang tak terduga. Sisi kontra: sebagian alat lama yang mengandalkan pewarisan untuk memperluas perilaku — termasuk beberapa pembuat test double — bisa tersandung, dan membuka class di kemudian hari hanya perubahan satu kata yang murah.

php
<?php
final class Kurs
{
    public function __construct(public readonly float $nilai)
    {
    }
}

// Baris berikut membuat skrip gagal dimuat bila komentarnya dibuka:
// class KursBank extends Kurs {}

Pilihan yang sehat untuk kode aplikasi: mulai dari final, dokumentasikan titik perluasan yang memang disengaja, lalu buka class secara sadar — bukan karena lupa menutupnya. Untuk pustaka publik, keputusan ini lebih politis: pelanggan Anda yang menentukan ekstensibilitas, jadi dengarkan mereka sebelum mengunci.

Anti-Pola yang Perlu Dihindari #

Empat kebiasaan berikut paling sering muncul ketika object model dipakai tanpa disiplin. Gunakan tabel ini sebagai alat pemindaian cepat saat mereview kode:

Anti-polaCiri umumPenanganan
God objectSatu class menampung puluhan metode lintas domainPecah menurut tanggung jawab; satu class satu alasan berubah
Primitive obsessionstring dan array mewakili konsep domain (kode voucher, persen diskon)Angkat menjadi value object final + readonly yang memvalidasi diri
Feature envyMetode class A sibuk membaca-mengubah data class BPindahkan metode ke class pemilik data tersebut
Static service locatorObject mengambil dependensi lewat panggilan statis globalSuntikkan dependensi eksplisit lewat constructor

Baris primitive obsession layak diperlihatkan wujud penanganannya karena paling sering dianggap sepele. Dua value object kecil berikut memindahkan validasi dari tempat pemakaian ke tempat definisi:

php
<?php
final class KodeVoucher
{
    public function __construct(public readonly string $kode)
    {
        if (!preg_match('/^[A-Z]{2}-\d{4}$/', $kode)) {
            throw new InvalidArgumentException("Format voucher salah: $kode");
        }
    }
}

final class PersenDiskon
{
    public function __construct(public readonly float $nilai)
    {
        if ($nilai < 0.0 || $nilai > 100.0) {
            throw new InvalidArgumentException('Persen harus di antara 0 dan 100');
        }
    }
}

function hitungHargaAkhir(float $harga, PersenDiskon $diskon): float
{
    return $harga - ($harga * $diskon->nilai / 100);
}

$voucher = new KodeVoucher('HM-2026');
$diskon = new PersenDiskon(20.0);
echo $voucher->kode . ': Rp' . hitungHargaAkhir(150000, $diskon) . "\n";

try {
    new PersenDiskon(150.0);
} catch (InvalidArgumentException $e) {
    echo 'Ditahan: ' . $e->getMessage() . "\n";
}

Output yang Dihasilkan:

text
HM-2026: Rp120000
Ditahan: Persen harus di antara 0 dan 100

Fungsi hitungHargaAkhir(float $harga, PersenDiskon $diskon) tidak lagi menerima persen telanjang: setiap nilai sudah terjamin berada di antara 0 dan 100 sejak dibangun, dan tertukarnya argumen KodeVoucher dengan PersenDiskon tertangkap sebagai TypeError, bukan siluman logika. Bandingkan dengan versi (float $harga, float $persen, string $kode) — tiga primitif yang menunggu untuk tertukar dan lolos tanpa validasi apa pun.

Daftar Periksa Sebelum Merge #

Daftar berikut memadatkan chapter ini — dan sebagian besar section — menjadi pertanyaan ya/tidak yang bisa dijawab cepat:

  • Apakah object yang merepresentasikan nilai domain bersifat immutable (readonly, tanpa setter)?
  • Apakah setiap class punya satu alasan utama untuk berubah?
  • Apakah dependensi disuntikkan lewat constructor, bukan diambil dari state global?
  • Apakah antarmuka yang diminta setiap konsumen adalah antarmuka terkecil yang cukup?
  • Apakah kegagalan direpresentasikan sebagai exception yang bermakna, bukan null atau false senyap?
  • Apakah tipe property, parameter, dan nilai kembalian selalu dideklarasikan?
  • Apakah koleksi besar diproses lewat iterable atau generator, bukan array raksasa?
  • Apakah registry berumur panjang memakai WeakMap bila menampung object milik pihak lain?
  • Apakah class baru dimulai dari final dan hanya dibuka secara sadar?
  • Apakah nama class dan metode masih deskriptif saat dibaca orang luar tim?

Tidak ada butir yang mekanis — semuanya melindungi satu hal: kemudahan berubah di masa depan. Kode yang lolos daftar periksa ini biasanya juga lolos review lebih cepat.

Ringkasan #

Praktik terbaik object model PHP berputar pada beberapa tema: state yang kecil dan terkontrol (immutabilitas, anti-statis), relasi yang longgar (komposisi, antarmuka kecil, dependency injection), serta kegagalan yang berbicara lantang (exception, readonly, final). Fitur bahasa yang kita pelajari sepanjang section ini hanyalah kosakata — prinsip di chapter inilah yang menyusun kosakata tersebut menjadi kalimat yang sehat. Section Advanced Object Model telah tuntas; perjalanan belajar Anda berlanjut ke section berikutnya dengan bekal object model yang kini dikuasai sepenuhnya.

Referensi #