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
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
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:
- Baris 5 (
public string $email { ... }): property tetap dideklarasikanpublicdengan tipestring, lalu diikuti blok hooks di dalam kurung kurawal milik property tersebut. - Baris 6 (
set (string $nilai)): hooksetberjalan setiap kali property ditulis. Parameter$nilaimenampung nilai yang sedang ditugaskan; tipe parameter cocok dengan tipe property. - Baris 7-10: validasi tetap dilakukan seperti versi method — nilai yang bukan email valid melempar
InvalidArgumentException. - Baris 10 (
$this->email = strtolower($nilai)): penulisan$this->emaildari di dalam hook-nya sendiri menulis nilai simpanan (backing value) secara langsung, bukan memanggil hooksetlagi — tidak ada perulangan tak berujung di sini. - Hook
gettidak ditulis: ketika hanya hooksetyang didefinisikan, PHP otomatis membuatkan hookgetsederhana yang membaca nilai simpanan. Itulah sebabnya baris 21 bisa membaca$akun->emailsecara langsung. - Baris 16 (
$this->email = $emaildi constructor): penugasan di dalam class juga melewati hookset, sehingga validasi dan normalisasi sudah berjalan sejak konstruksi objek.
Sekarang mari kita sengaja gagalkan validasinya untuk melihat jejak error-nya:
<?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:
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 8Perhatikan 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
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:
80000Bagian penting dari contoh ini:
- Baris 10-12: property
$hargaSetelahDiskonhanya memiliki hookgetberbentuk panah=>— bentuk singkat untuk hook satu ekspresi, setara denganget { return $this->harga * (1 - $this->diskon); }. - Hook
getmembaca$this->hargadan$this->diskon, bukan$this->hargaSetelahDiskon— karena tidak ada nilai simpanan yang bisa dibaca, property ini murni hasil perhitungan. - Baris 15-16: dari luar class,
$produk->hargaSetelahDiskonterlihat dan terasa seperti property biasa bernilai80000, 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
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:
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 16Pesan 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
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:
36.67
36.61Tiga hal penting dari kode di atas:
- Baris 6 (
get => $this->celcius;): pembacaan$this->celciusdari dalam hook miliknya sendiri membaca nilai simpanan mentah, bukan memanggil hookgetlagi — PHP sengaja mengecualikan akses dari dalam hook agar tidak terjadi rekursi. - Baris 7 (
set (float $nilai) => ...): hooksetmemakai bentuk panah karena isinya satu ekspresi saja, yaitu membulatkan nilai ke dua desimal sebelum disimpan. - Baris 16-20:
36.66666tersimpan sebagai36.67dan36.611sebagai36.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.
| Jenis | Ciri | Contoh |
|---|---|---|
| Backed property | Hook membaca/menulis $this->property miliknya sendiri | $email, $celcius |
| Virtual property | Hook 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
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:
[Siti]Mari kita bedah kontrak ini:
- Baris 5 (
public string $nama { get; set; }): interfaceBisaDinamaimenuntut property$namayang bisa dibaca dan ditulis — kedua hook ditulis sebagai daftar singkat tanpa badan. - Baris 10-14: class
Penggunamemenuhi tuntutansetdengan hook yang memangkas spasi di awal dan akhir nilai. Hookgettidak perlu ditulis ulang karena versi otomatis dari PHP sudah memenuhi kontrakgetinterface. - Baris 22: hasil cetak
[Siti]— tanpa spasi di awal dan akhir — membuktikan hooksetberjalan: 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
staticditolak dengan pesanCannot declare hooks for static property. - Property dengan hooks tidak boleh
readonly. Kombinasi keduanya ditolak denganHooked properties cannot be readonly— hooksetdanreadonlymemang saling bertentangan: satu mengatur jalannya penulisan, satu melarang penulisan. Untuk data immutable yang butuh komputasi, gunakan method biasa seperti polawithpada Readonly Classes . - Tipe hook harus konsisten. Tipe parameter
$nilaipada hooksetharus cocok dengan tipe property, sepertistringuntukpublic 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.