Property public memang mudah dipakai: siapa pun boleh membacanya. Namun kemudahan tersebut berlaku dua arah — kode luar juga bisa mengubah nilainya kapan saja, dan satu penugasan yang lalai cukup untuk merusak state sebuah object. Selama ini pilihan kita hanya dua: public yang terbuka penuh, atau private yang memaksa kita menulis getter agar nilai masih terbaca. PHP 8.4 memperkenalkan asymmetric visibility: visibility untuk operasi baca dan tulis dipisahkan, sehingga sebuah property bisa dibaca dari mana saja tetapi hanya boleh ditulis dari scope class. Pada chapter ini kita pelajari sintaksnya, kombinasi dengan constructor promotion dan property hooks, hingga pola imutabilitas yang lebih ringkas daripada readonly.

Masalah Baca Bebas dan Tulis Terkendali #

Kebutuhan “boleh dibaca siapa saja, tapi hanya boleh diubah pemiliknya” muncul hampir di semua domain: saldo rekening, status pesanan, atau harga produk. PHP 8.4 memenuhi kebutuhan tersebut dengan memperluas sintaks deklarasi property. Mari kita bandingkan dulu pendekatan yang tersedia sebelumnya:

PendekatanBaca dari luarTulis dari luarCatatan
publicbolehbolehtidak ada kontrol penulisan sama sekali
private + getter methodlewat methodtidak bisamenambah boilerplate getter
readonlybolehtidak bisahanya boleh diinisialisasi satu kali
public private(set)bolehtidak bisatulis hanya dari scope class, boleh berulang

Asymmetric visibility mengisi celah di baris terakhir: properti tetap terbuka untuk dibaca, sementara penulisan dikunci di dalam class. Berbeda dengan readonly yang hanya mengizinkan inisialisasi satu kali (lihat chapter readonly properties ), property dengan asymmetric visibility boleh ditulis ulang berkali-kali — asalkan dari scope yang berizin.

Sintaks public private(set) #

Bentuk paling umum adalah public private(set). Kata visibility pertama mengatur operasi baca, sedangkan kata di dalam tanda kurung beserta akhiran (set) mengatur operasi tulis:

php
<?php

class Rekening
{
    public private(set) int $saldo;

    public function __construct(int $saldoAwal)
    {
        $this->saldo = $saldoAwal;
    }

    public function tambahSaldo(int $jumlah): void
    {
        $this->saldo += $jumlah;
    }
}

$rekening = new Rekening(500000);
echo "Saldo awal: {$rekening->saldo}\n";

$rekening->tambahSaldo(250000);
echo "Saldo akhir: {$rekening->saldo}\n";

Output yang Dihasilkan:

text
Saldo awal: 500000
Saldo akhir: 750000

Mari kita telaah baris-baris pentingnya:

  1. Baris 4 (public private(set) int $saldo;): property $saldo deklarasikan dengan visibility baca public dan visibility tulis private(set). Bagian (set) inilah penanda asymmetric visibility — tanpa suffix tersebut, private biasa akan mengunci baca dan tulis sekaligus.
  2. Baris 7 ($this->saldo = $saldoAwal;): constructor berada di dalam scope class Rekening, sehingga penulisan diperbolehkan.
  3. Baris 12 ($this->saldo += $jumlah;): method tambahSaldo() juga berada dalam scope class, jadi compound assignment baca-tulis di sini sah.
  4. Baris 17 dan 20 ($rekening->saldo): kode luar class membaca $saldo sebagaimana property public biasa — tanpa getter, tanpa method tambahan.

Penulisan dari Luar Class Berakhir Error #

Sekarang kita uji sisi penguncianya. Program berikut sengaja menulis $rekening->saldo dari scope global:

php
<?php

class Rekening
{
    public private(set) int $saldo = 100000;
}

$rekening = new Rekening();
echo "Baca saldo: {$rekening->saldo}\n";
$rekening->saldo = 500000;

Output yang Dihasilkan:

text
Baca saldo: 100000

Fatal error: Uncaught Error: Cannot modify private(set) property Rekening::$saldo from global scope in /home/z/my-project/php-course/content/php/advanced-object-model/asymmetric-visibility/.verify_tmp.php:10
Stack trace:
#0 {main}
  thrown in /home/z/my-project/php-course/content/php/advanced-object-model/asymmetric-visibility/.verify_tmp.php on line 10

Baris echo berhasil mencetak saldo karena operasi baca memang public. Baris terakhir kemudian melempar Error: Cannot modify private(set) property Rekening::$saldo from global scope — PHP menolak penulisan dari scope yang tidak berizin, dan eksekusi berhenti dengan exit code 255. Kesalahan ini terdeteksi saat runtime, bukan saat parsing, jadi lindungi invarian class Anda dengan disiplin scope, bukan dengan berharap kesalahan tertangkap lebih awal.

Dengan Constructor Promotion #

Asymmetric visibility bisa langsung dipakai pada constructor property promotion . Kita cukup menuliskan kedua visibility pada parameter yang dipromosikan:

php
<?php

class Produk
{
    public function __construct(
        public private(set) string $nama,
        public private(set) int $harga,
    ) {}
}

$produk = new Produk("Kopi Gayo", 85000);
echo "{$produk->nama} seharga Rp{$produk->harga}\n";

Output yang Dihasilkan:

text
Kopi Gayo seharga Rp85000

Parameter public private(set) string $nama otomatis menjadi property $nama dengan aturan baca-tulis terpisah. Constructor sendiri berjalan di dalam scope class, sehingga inisialisasi awal oleh promotion selalu diperbolehkan tanpa konflik. Hasilnya adalah value object ringkas: tiga baris untuk mendapatkan property yang terbaca publik namun terkunci penulisannya.

Imutabilitas Tanpa readonly #

Pola wither — membuat salinan object dengan satu nilai berubah — menjadi jauh lebih ringan dengan asymmetric visibility. Karena clone dijalankan dari dalam method class, kita boleh menulis property hasil salinan tersebut:

php
<?php

class Uang
{
    public function __construct(public private(set) int $jumlah) {}

    public function tambah(int $tambahan): static
    {
        $baru = clone $this;
        $baru->jumlah = $this->jumlah + $tambahan;
        return $baru;
    }
}

$a = new Uang(1000);
$b = $a->tambah(500);

echo "Objek lama: Rp{$a->jumlah}\n";
echo "Objek baru: Rp{$b->jumlah}\n";

Output yang Dihasilkan:

text
Objek lama: Rp1000
Objek baru: Rp1500

Method tambah() membuat salinan lewat clone $this, mengubah $jumlah milik salinan (sah karena masih dalam scope Uang), lalu mengembalikan object baru. Object asli $a tidak pernah tersentuh — perhatikan nilainya tetap 1000 setelah pemanggilan. Dengan readonly pola serupa harus menunggu kelonggaran aturan di PHP 8.3 (reinisialisasi di dalam __clone) atau trik tambahan; dengan private(set), cukup clone, tulis, dan kembalikan.

Varian protected(set) dan Warisan #

Varian protected(set) membuka penulisan untuk scope class beserta seluruh subclass-nya:

php
<?php

class Entitas
{
    public function __construct(public protected(set) string $slug) {}

    protected function ubahSlug(string $slugBaru): void
    {
        $this->slug = $slugBaru;
    }
}

class Artikel extends Entitas
{
    public function publikasiUlang(): string
    {
        $this->ubahSlug("belajar-php-84-v2");
        return $this->slug;
    }
}

$artikel = new Artikel("belajar-php-84");
echo "Sebelum: {$artikel->slug}\n";
echo "Sesudah: " . $artikel->publikasiUlang() . "\n";

Output yang Dihasilkan:

text
Sebelum: belajar-php-84
Sesudah: belajar-php-84-v2

Method publikasiUlang() di class Artikel berhasil memodifikasi $slug karena visibility tulis protected(set) mengikuti aturan protected pada umumnya. Perlu dicatat, ubahSlug() sengaja diletakkan di parent dengan visibility protected — subclass memanggilnya alih-alih menulis $this->slug langsung, yang menunjukkan desain di mana parent mengendalikan bagaimana perubahan terjadi.

Sebaliknya, private(set) lebih ketat daripada yang diperkirakan sebagian pembaca: hanya scope class pendeklarasi yang boleh menulis. Subclass tetap terkunci:

php
<?php

class Entitas
{
    public private(set) string $slug = "artikel-baru";
}

class Artikel extends Entitas
{
    public function gantiSlug(): void
    {
        $this->slug = "artikel-lama";
    }
}

(new Artikel())->gantiSlug();

Output yang Dihasilkan:

text

Fatal error: Uncaught Error: Cannot modify private(set) property Entitas::$slug from scope Artikel in /home/z/my-project/php-course/content/php/advanced-object-model/asymmetric-visibility/.verify_tmp.php:12
Stack trace:
#0 /home/z/my-project/php-course/content/php/advanced-object-model/asymmetric-visibility/.verify_tmp.php(16): Artikel->gantiSlug()
#1 {main}
  thrown in /home/z/my-project/php-course/content/php/advanced-object-model/asymmetric-visibility/.verify_tmp.php on line 12

Meskipun property $slug terbaca publik dan class Artikel mewarisinya, penulisan dari method subclass tetap ditolak dengan Error ... from scope Artikel. Jika subclass memang perlu mengubah nilainya, gunakan protected(set) sejak awal atau sediakan method pengubah di class pendeklarasi.

Kombinasi dengan Property Hooks #

Asymmetric visibility dirancang saling melengkapi dengan property hooks . Kedua fitur bisa dipasang pada satu property sekaligus — visibility mengatur siapa yang boleh menulis, hook mengatur apa yang terjadi ketika penulisan itu sah:

php
<?php

class Harga
{
    // get publik, set hanya dari scope class, dan melewati hook set
    public private(set) int $nilai {
        set (int $baru) {
            if ($baru < 0) {
                throw new InvalidArgumentException("Harga tidak boleh negatif");
            }
            $this->nilai = $baru;
        }
    }
}

Hook set pada contoh di atas memvalidasi bahwa nilai baru tidak negatif; penulisan dari luar class tetap ditolak oleh private(set) sebelum hook sempat berjalan. Aturan mainnya satu arah: visibility tulis tidak boleh lebih longgar daripada visibility baca — penulisan private public(set) ... langsung ditolak PHP dengan pesan visibility of property must not be weaker than set visibility.

Ringkasan #

Asymmetric visibility memberi PHP kontrol granular yang selama ini mengharuskan kita memilih antara keterbukaan dan keamanan: public private(set) untuk nilai yang bebas dibaca namun dikunci penulisannya, protected(set) bila hierarki class perlu ikut menulis, dan kombinasi dengan hooks untuk validasi setiap penulisan yang sah. Fitur ini melengkapi — bukan menggantikan — readonly: gunakan readonly untuk nilai yang benar-benar tidak berubah, dan asymmetric visibility untuk nilai yang berubah hanya lewat kontrol class. Berikutnya kita berpindah dari pengelolaan property ke pengelolaan sekumpulan nilai tetap: enumeration.

Referensi #