Property yang baik sering kali tidak cukup sekadar menyimpan nilai: email harus divalidasi dan diseragamkan huruf kecilnya, nama depan perlu dipangkas dari spasi, dan harga akhir dihitung dari harga dasar dikurangi diskon. Selama dua puluh tahun, satu-satunya jalan PHP adalah memprivatkan property lalu membungkusnya dengan method getX() dan setX(). PHP 8.4 akhirnya membawa solusi native bernama property hooks: logika get dan set yang menempel langsung pada deklarasi property, mengikuti jejak fitur accessor di C# dan Kotlin. Pada chapter ini kita bedah hook set untuk validasi, hook get untuk property terhitung, kombinasi keduanya, hingga penggunaan hooks pada interface.

Masalah Getter dan Setter Manual #

Mari kita mulai dari pola lama yang ingin digantikan. Class berikut menjaga property $email tetap bersih lewat sepasang method:

php
<?php

class AkunLama
{
    private string $email;

    public function __construct(string $email)
    {
        $this->setEmail($email);
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setEmail(string $email): void
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Email tidak valid: $email");
        }
        $this->email = strtolower($email);
    }
}

$akun = new AkunLama("[email protected]");
echo $akun->getEmail() . "\n";

Output yang Dihasilkan:

Pola ini berfungsi, tetapi mahal untuk dipelihara: property harus dijadikan private, dua method ditulis untuk satu field, dan setiap pemanggil berganti dari akses property biasa menjadi pemanggilan method getEmail() dan setEmail(). Untuk class dengan banyak field yang butuh perlakuan serupa, jumlah method getter/setter meledak jauh melampaui logika bisnis aslinya.

Hook set untuk Validasi dan Normalisasi #

Property hooks memindahkan logika tersebut ke tempat yang paling natural: deklarasi property itu sendiri. Perhatikan versi hooks dari class yang sama:

php
<?php

class Akun
{
    public string $email {
        set (string $nilai) {
            if (!filter_var($nilai, FILTER_VALIDATE_EMAIL)) {
                throw new InvalidArgumentException("Email tidak valid: $nilai");
            }
            $this->email = strtolower($nilai);
        }
    }

    public function __construct(string $email)
    {
        $this->email = $email;
    }
}

$akun = new Akun("[email protected]");
echo $akun->email . "\n";

Output yang Dihasilkan:

Mari kita bedah bagian-bagiannya:

  1. Baris 5 (public string $email { ... }): property tetap dideklarasikan public dengan tipe string, lalu diikuti blok hooks di dalam kurung kurawal milik property tersebut.
  2. Baris 6 (set (string $nilai)): hook set berjalan setiap kali property ditulis. Parameter $nilai menampung nilai yang sedang ditugaskan; tipe parameter cocok dengan tipe property.
  3. Baris 7-10: validasi tetap dilakukan seperti versi method — nilai yang bukan email valid melempar InvalidArgumentException.
  4. Baris 10 ($this->email = strtolower($nilai)): penulisan $this->email dari di dalam hook-nya sendiri menulis nilai simpanan (backing value) secara langsung, bukan memanggil hook set lagi — tidak ada perulangan tak berujung di sini.
  5. Hook get tidak ditulis: ketika hanya hook set yang didefinisikan, PHP otomatis membuatkan hook get sederhana yang membaca nilai simpanan. Itulah sebabnya baris 21 bisa membaca $akun->email secara langsung.
  6. Baris 16 ($this->email = $email di constructor): penugasan di dalam class juga melewati hook set, sehingga validasi dan normalisasi sudah berjalan sejak konstruksi objek.

Sekarang mari kita sengaja gagalkan validasinya untuk melihat jejak error-nya:

php
<?php

class Akun
{
    public string $email {
        set (string $nilai) {
            if (!filter_var($nilai, FILTER_VALIDATE_EMAIL)) {
                throw new InvalidArgumentException("Email tidak valid: $nilai");
            }
            $this->email = strtolower($nilai);
        }
    }

    public function __construct(string $email)
    {
        $this->email = $email;
    }
}

$akun = new Akun("budi#example.com");

Output yang Dihasilkan:

text

Fatal error: Uncaught InvalidArgumentException: Email tidak valid: budi#example.com in /home/z/my-project/php-course/content/php/advanced-object-model/property-hooks/.verify_tmp.php:8
Stack trace:
#0 /home/z/my-project/php-course/content/php/advanced-object-model/property-hooks/.verify_tmp.php(16): Akun->$email::set('budi#example.co...')
#1 /home/z/my-project/php-course/content/php/advanced-object-model/property-hooks/.verify_tmp.php(20): Akun->__construct('budi#example.co...')
#2 {main}
  thrown in /home/z/my-project/php-course/content/php/advanced-object-model/property-hooks/.verify_tmp.php on line 8

Perhatikan baris stack trace Akun->$email::set('budi#example.co...'): PHP menampilkan hook yang sedang berjalan sebagai frame tersendiri, sehingga saat debugging kita bisa melihat bahwa kegagalan terjadi di hook set property $email, dipicu oleh constructor.

Hook get dan Property Virtual #

Hook get mengambil alih saat property dibaca. Kegunaan paling menariknya adalah virtual property: property yang tidak memiliki nilai simpanan sendiri karena nilainya selalu dihitung dari property lain:

php
<?php

class Produk
{
    public function __construct(
        public float $harga,
        public float $diskon = 0.0,
    ) {}

    public float $hargaSetelahDiskon {
        get => $this->harga * (1 - $this->diskon);
    }
}

$produk = new Produk(100000.0, 0.2);
echo $produk->hargaSetelahDiskon . "\n";

Output yang Dihasilkan:

text
80000

Bagian penting dari contoh ini:

  1. Baris 10-12: property $hargaSetelahDiskon hanya memiliki hook get berbentuk panah => — bentuk singkat untuk hook satu ekspresi, setara dengan get { return $this->harga * (1 - $this->diskon); }.
  2. Hook get membaca $this->harga dan $this->diskon, bukan $this->hargaSetelahDiskon — karena tidak ada nilai simpanan yang bisa dibaca, property ini murni hasil perhitungan.
  3. Baris 15-16: dari luar class, $produk->hargaSetelahDiskon terlihat dan terasa seperti property biasa bernilai 80000, padahal setiap pembacaan menghitung ulang dari harga dan diskon terkini.

Property virtual tidak bisa ditulis karena tidak ada tempat menyimpan nilainya. PHP menolak penulisan dengan pesan yang jelas:

php
<?php

class Produk
{
    public function __construct(
        public float $harga,
        public float $diskon = 0.0,
    ) {}

    public float $hargaSetelahDiskon {
        get => $this->harga * (1 - $this->diskon);
    }
}

$produk = new Produk(100000.0, 0.2);
$produk->hargaSetelahDiskon = 50000.0;

Output yang Dihasilkan:

text

Fatal error: Uncaught Error: Property Produk::$hargaSetelahDiskon is read-only in /home/z/my-project/php-course/content/php/advanced-object-model/property-hooks/.verify_tmp.php:16
Stack trace:
#0 {main}
  thrown in /home/z/my-project/php-course/content/php/advanced-object-model/property-hooks/.verify_tmp.php on line 16

Pesan Property Produk::$hargaSetelahDiskon is read-only muncul karena satu-satunya cara mengubah hasil hitungnya adalah mengubah $harga atau $diskon — sumber datanya.

Menggabungkan get dan set #

Property dengan hooks bisa memiliki kedua hook sekaligus. Pola ini biasa dipakai untuk normalisasi dua arah: bentuk simpanan dirapikan saat masuk, lalu dibaca kembali saat keluar:

php
<?php

class Suhu
{
    public float $celcius {
        get => $this->celcius;
        set (float $nilai) => $this->celcius = round($nilai, 2);
    }

    public function __construct(float $celcius)
    {
        $this->celcius = $celcius;
    }
}

$ukur1 = new Suhu(36.66666);
echo $ukur1->celcius . "\n";

$ukur2 = new Suhu(36.611);
echo $ukur2->celcius . "\n";

Output yang Dihasilkan:

text
36.67
36.61

Tiga hal penting dari kode di atas:

  1. Baris 6 (get => $this->celcius;): pembacaan $this->celcius dari dalam hook miliknya sendiri membaca nilai simpanan mentah, bukan memanggil hook get lagi — PHP sengaja mengecualikan akses dari dalam hook agar tidak terjadi rekursi.
  2. Baris 7 (set (float $nilai) => ...): hook set memakai bentuk panah karena isinya satu ekspresi saja, yaitu membulatkan nilai ke dua desimal sebelum disimpan.
  3. Baris 16-20: 36.66666 tersimpan sebagai 36.67 dan 36.611 sebagai 36.61; pembulatan terjadi otomatis pada setiap penulisan, termasuk dari dalam constructor.

Istilah resminya: property seperti $celcius dan $email disebut backed property karena hook-nya menyentuh nilai simpanan ($this->properti itu sendiri), sedangkan $hargaSetelahDiskon disebut virtual property karena hook-nya hanya membaca property lain.

JenisCiriContoh
Backed propertyHook membaca/menulis $this->property miliknya sendiri$email, $celcius
Virtual propertyHook hanya menyentuh property lain; tidak ada nilai simpanan$hargaSetelahDiskon

Hooks pada Interface #

Kekuatan lain property hooks: interface bisa mendeklarasikan property dengan hooks sebagai kontrak. Class yang mengimplementasikan interface tersebut wajib menyediakan property dengan perilaku serupa:

php
<?php

interface BisaDinamai
{
    public string $nama { get; set; }
}

class Pengguna implements BisaDinamai
{
    public string $nama {
        set (string $nilai) {
            $this->nama = trim($nilai);
        }
    }

    public function __construct(string $nama)
    {
        $this->nama = $nama;
    }
}

$pengguna = new Pengguna("  Siti  ");
echo "[" . $pengguna->nama . "]\n";

Output yang Dihasilkan:

text
[Siti]

Mari kita bedah kontrak ini:

  1. Baris 5 (public string $nama { get; set; }): interface BisaDinamai menuntut property $nama yang bisa dibaca dan ditulis — kedua hook ditulis sebagai daftar singkat tanpa badan.
  2. Baris 10-14: class Pengguna memenuhi tuntutan set dengan hook yang memangkas spasi di awal dan akhir nilai. Hook get tidak perlu ditulis ulang karena versi otomatis dari PHP sudah memenuhi kontrak get interface.
  3. Baris 22: hasil cetak [Siti] — tanpa spasi di awal dan akhir — membuktikan hook set berjalan: nilai yang masuk berupa " Siti ", dan yang tersimpan sudah bersih.

Dengan kontrak seperti ini, kode lain bisa bergantung pada perilaku property — “nama selalu dipangkas” — tanpa peduli class mana yang menyediakannya, menempatkan hooks setara dengan method pada pola pemrograman berbasis interface.

Batasan yang Perlu Diketahui #

PHP memagari property hooks dengan beberapa aturan (semua pesan berikut kami uji langsung pada PHP 8.4):

  • Hook hanya untuk property instance. Hook pada property static ditolak dengan pesan Cannot declare hooks for static property.
  • Property dengan hooks tidak boleh readonly. Kombinasi keduanya ditolak dengan Hooked properties cannot be readonly — hook set dan readonly memang saling bertentangan: satu mengatur jalannya penulisan, satu melarang penulisan. Untuk data immutable yang butuh komputasi, gunakan method biasa seperti pola with pada Readonly Classes .
  • Tipe hook harus konsisten. Tipe parameter $nilai pada hook set harus cocok dengan tipe property, seperti string untuk public string $email.

Ringkasan #

Property hooks menghapus boilerplate getter/setter dengan menempelkan logika get dan set langsung pada property: hook set menjaga mutu data yang masuk, hook get menyajikan property terhitung tanpa nilai simpanan, dan interface bisa menjadikan perilaku property sebagai kontrak. Batasannya jelas — hanya property instance, tidak boleh readonly — agar kegunaannya tetap terfokus pada data. Kontrol akses selang-selang antara baca dan tulis masih menjadi pekerjaan visibility; asymmetric visibility di chapter berikutnya melengkapi bagian terakhir dari teka-teki ini.

Referensi #