Banyak class ingin memberlakukan aturan sederhana: semua orang boleh membaca, hanya class sendiri yang boleh menulis. Property public memenuhi separuh syarat itu dan melanggar separuhnya; property private dengan getter memenuhi keduanya dengan harga boilerplate; readonly terlalu kaku karena membekukan selamanya padahal class kadang perlu memperbarui nilai secara internal. PHP 8.4 menutup celah ini dengan asymmetric visibility — modifier seperti private(set) yang membedakan visibilitas operasi tulis dari operasi baca. Chapter ini menelusuri evolusinya dari pola wither berbasis clone yang dulu menjadi satu-satunya pilihan, menuju bentuk satu kata yang menggantikannya. Aturan interaksi dengan pewarisan, reflection, dan serialisasi dibahas di chapter asymmetric visibility di Section Advanced Object Model .

Kebutuhan Lama: Baca Bebas, Tulis Terbatas #

Mari kita pertegas masalahnya dengan object keranjang belanja. Item dibangun sekali, dibaca di mana saja — subtotal dihitung dari $jumlah * $harga — tetapi perubahan jumlah harus terkendali karena ada logika persediaan yang ingin ditanam di kemudian hari. Sebelum PHP 8.4, pengaturan seperti ini hanya punya dua jalan yang sama-sama tidak nyaman.

Jalan pertama adalah membiarkan property tetap public lalu menyediakan method salin-dan-ubah — pola wither berbasis clone:

php
<?php
class ItemKeranjangLama
{
    public function __construct(
        public string $produk,
        public int $jumlah,
        public int $harga,
    ) {}

    public function withJumlah(int $jumlah): static
    {
        $salinan = clone $this;
        $salinan->jumlah = $jumlah;
        return $salinan;
    }
}

$awal = new ItemKeranjangLama('Kopi Tubruk', 1, 25000);
$baru = $awal->withJumlah(3);

echo $awal->jumlah, ' x ', $awal->produk, ' = ', number_format($awal->jumlah * $awal->harga, 0, ',', '.'), "\n";
echo $baru->jumlah, ' x ', $baru->produk, ' = ', number_format($baru->jumlah * $baru->harga, 0, ',', '.'), "\n";

Output yang Dihasilkan:

text
1 x Kopi Tubruk = 25.000
3 x Kopi Tubruk = 75.000

Method withJumlah() menyalin object lewat clone, menimpa $jumlah pada salinan, lalu mengembalikannya — object $awal tidak tersentuh dan object $baru berdiri sendiri. Pola ini benar, tetapi bersifat anjuran: property $jumlah yang public tetap bisa ditulis langsung dari mana saja ($awal->jumlah = 99;), melewati seluruh pagar yang kita susun. Jalan kedua — private plus getter plus setter — mengunci tulisan dengan benar, tetapi membuang kemudahan baca langsung dan menuntut tiga deklarasi per property.

private(set) di PHP 8.4 #

Asymmetric visibility memperkenalkan modifier tulis yang berdiri di samping visibilitas baca. Sintaksnya: visibilitas utama, lalu visibilitas khusus set dalam tanda kurung:

php
<?php
class ItemKeranjang
{
    public function __construct(
        public private(set) string $produk,
        public private(set) int $jumlah,
        public private(set) int $harga,
    ) {}

    public function denganJumlah(int $jumlah): static
    {
        return new self($this->produk, $jumlah, $this->harga);
    }
}

$awal = new ItemKeranjang('Kopi Tubruk', 1, 25000);
echo 'Baca dari luar: ', $awal->produk, "\n";

$baru = $awal->denganJumlah(3);
echo 'Hasil wither: ', $baru->jumlah, ' x ', $baru->produk, "\n";
echo 'Object awal tetap: ', $awal->jumlah, ' x ', $awal->produk, "\n";

Output yang Dihasilkan:

text
Baca dari luar: Kopi Tubruk
Hasil wither: 3 x Kopi Tubruk
Object awal tetap: 1 x Kopi Tubruk

Mari kita bedah perubahan pentingnya:

  1. public private(set) int $jumlah: baca dari mana saja (public), tulis hanya dari dalam class ItemKeranjang sendiri (private(set)).
  2. Baris pembacaan: $awal->produk dan $baru->jumlah tetap dibaca langsung seperti property biasa — tidak ada getter di tengah jalan.
  3. denganJumlah(): method wither kini membangun new self(...) karena class boleh menulis property-nya sendiri; output menunjukkan object awal tetap 1 x dan hasil wither menjadi 3 x.

Perbandingannya tajam: dibanding pola clone lama, method wither menyusut satu baris dan kehilangan langkah penimpaan manual; dibanding versi private-plus-setter, seluruh baris deklarasi berkurang drastis. Satu kata kunci menutup celah yang tadinya mengharuskan memilih antara kebebasan berlebih dan boilerplate.

Penulisan dari Luar Ditolak #

Jaminannya bisa diuji langsung. Coba tulis property private(set) dari scope global:

php
<?php
class ItemKeranjang
{
    public function __construct(
        public private(set) string $produk,
        public private(set) int $jumlah,
    ) {}
}

$item = new ItemKeranjang('Kopi Tubruk', 1);
$item->jumlah = 99;

Output yang Dihasilkan:

text

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

Penugasan $item->jumlah = 99; berakhir Fatal error dengan pesan Cannot modify private(set) property ItemKeranjang::$jumlah from global scope. Pesan ini menarik perhatian kita pada satu hal: yang ditolak adalah penulisan — jika baris tersebut hanya membaca (echo $item->jumlah;), eksekusi berjalan normal. Satu property, dua visibilitas, dua nasib operasi.

Variasi protected(set) dan Kombinasi dengan Hooks #

private(set) bukan satu-satunya pilihan. protected(set) membuka hak tulis bagi class anak — berguna ketika induk ingin menjaga kontrak baca sambil membiarkan turunan mengelola isi; sementara varian paling longgar (set) (tanpa visibilitas tambahan) adalah perilaku lama: tulis seleluas baca. Hierarki lengkapnya:

DeklarasiBacaTulis
public $xsiapa sajasiapa saja
public protected(set) $xsiapa sajaclass sendiri dan turunannya
public private(set) $xsiapa sajahanya class sendiri
private $xhanya class sendirihanya class sendiri

Contoh berikut memperlihatkan protected(set) bekerja lintas pewarisan:

php
<?php
class Dokumen
{
    public function __construct(
        public protected(set) string $judul = 'Tanpa judul',
    ) {}

    public function gantiJudul(string $judul): void
    {
        // Penulisan dari dalam class sendiri: sah
        $this->judul = $judul;
    }
}

class Laporan extends Dokumen
{
    public function judulLaporan(): string
    {
        // Class anak boleh menulis property protected(set) milik induk
        $this->judul = 'Laporan Bulanan';
        return $this->judul;
    }
}

$laporan = new Laporan();
echo $laporan->judul, "\n";
$laporan->gantiJudul('Laporan Tahunan');
echo $laporan->judul, "\n";
echo $laporan->judulLaporan(), "\n";

Output yang Dihasilkan:

text
Tanpa judul
Laporan Tahunan
Laporan Bulanan

Class induk menulis $this->judul dari method gantiJudul() dan class anak menulis dari judulLaporan() — keduanya sah karena keduanya berada dalam scope yang diizinkan protected(set). Kode di luar kedua class itu tetap hanya bisa membaca.

Fitur ini juga dirancang berdampingan dengan property hooks sesama fitur PHP 8.4 — keduanya kerap disebut lahir bersama. Modifier tulis ditulis sebelum kurung kurawal hook:

php
<?php
class Tag
{
    public private(set) string $label {
        get => ucfirst($this->label);
        set (string $nilai) {
            $this->label = strtolower(trim($nilai));
        }
    }

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

$tag = new Tag('  belajar php  ');
echo $tag->label, "\n";

Output yang Dihasilkan:

text
Belajar php

Constructor — berada di dalam class — boleh menulis $this->label, dan hook set ikut berjalan: nilai ' belajar php ' dipangkas lalu dijadikan huruf kecil sebelum disimpan. Pembacaan dari luar menembus hook get sehingga tampil Belajar php. Gabungan ini menghasilkan property yang terbaca bebas, terformat rapi, dan hanya bisa diubah lewat pintu yang class kontrol penuh.

Ringkasan #

Asymmetric visibility memberi PHP pengaturan hak yang selama ini hanya bisa dijenangkan lewat getter atau pola wither berbasis clone: public private(set) membaca bebas dan menulis tertutup, protected(set) membagi hak tulis ke turunan, dan semuanya berpadu mulus dengan property hooks. Pola wither tidak mati — justru menjadi lebih ringkas karena new self(...) di dalam class selalu sah. Dengan fitur ini, rangkaian fitur inti PHP 8.4 yang kita bahas sudah lengkap; materi penutup section ini akan merangkum peta fitur per versi PHP dan strategi menjaga kompatibilitas ketika proyek harus berjalan di beberapa versi sekaligus.

Referensi #