Pure function menjaga perilaku fungsi tetap terprediksi, tetapi prediksi itu rapuh kalau data yang sedang dioper bisa berubah diam-diam di tengah jalan. Immutability adalah kebiasaan memperlakukan data sebagai nilai tetap: alih-alih menimpa data lama, kita menghasilkan data baru untuk setiap perubahan. PHP tidak menyediakan kata kunci untuk “variabel terkunci” seperti const/let di JavaScript atau val di Kotlin, sehingga immutability di PHP dibangun dari tiga alat: salinan array, fungsi yang mengembalikan array baru, dan readonly class. Pada chapter ini kita akan membedah ketiganya satu per satu.

Data sebagai Nilai, Bukan Papan Tulis #

Sejak chapter Variabel kita tahu bahwa penugasan baru selalu menimpa isi variabel — dan memang tidak ada cara untuk melarangnya pada variabel biasa. Kata kunci const di PHP hanya berlaku untuk konstanta yang nilainya sudah tetap sejak penulisan kode, bukan untuk menjunci nilai hasil perhitungan. Konsekuensinya, immutability di PHP lebih merupakan pilihan desain: kita berhenti menulis kode yang menimpa, lalu memakai alat bahasa yang mendukung kebiasaan itu.

Kabar baiknya, untuk array — struktur data yang paling sering diolah secara fungsional — PHP berdiri di pihak kita.

Array PHP dan Copy-on-Write #

Penugasan array di PHP memakai value semantics: variabel tujuan menerima salinan isi array, bukan referensi ke array yang sama. PHP mengoptimalkannya dengan copy-on-write — salinan sungguhan baru dibuat ketika salah satu pihak mulai dimodifikasi, sehingga penugasan array tetap murah.

php
<?php

$daftarAwal = ['Kopi Gayo', 'Gula Aren'];
$daftarSalinan = $daftarAwal;

$daftarSalinan[] = 'Teh Melati';

echo implode(', ', $daftarAwal) . "\n";
echo implode(', ', $daftarSalinan) . "\n";

Output yang Dihasilkan:

text
Kopi Gayo, Gula Aren
Kopi Gayo, Gula Aren, Teh Melati

Mari kita telaah apa yang terjadi:

  1. $daftarSalinan = $daftarAwal mengikat kedua variabel ke isi yang sama secara murah — PHP hanya menandai bahwa array itu dimiliki dua pihak.
  2. $daftarSalinan[] = 'Teh Melati' adalah momen penulisan pertama pada salah satu pihak, sehingga PHP baru benar-benar menyalin array untuk variabel tersebut.
  3. $daftarAwal tetap berisi dua item — ia tidak pernah tahu bahwa salinannya sudah bertambah.

Perilaku ini adalah fondasi dari gaya “hasilkan data baru”: mengubah salinan tidak pernah menyengkelitkan data asal.

Memilih Fungsi yang Mengembalikan Array Baru #

Fungsi bawaan PHP terbagi dalam dua kubu: sebagian mengubah array asli in-place lewat referensi, sebagian mengembalikan array baru dan membiarkan aslinya utuh. Menyadari kubu mana sebuah fungsi berada adalah keterampilan kunci:

Mengubah array asli (in-place)Mengembalikan array baru
sort(), rsort(), asort(), ksort()array_map(), array_filter(), array_slice()
array_push($arr, $x), $arr[] = $xarray_merge($arr, [$x]), [...$arr, $x]
shuffle()tidak ada padanan langsung — salin dulu baru acak

Perbandingan berikut memperlihatkan kedua kubu tersebut bekerja pada array yang sama:

php
<?php

$hargaProduk = [85000, 25000, 45000];

// Mutasi in-place: sort() mengubah array yang sama lewat referensi.
$hargaUrut = $hargaProduk;
sort($hargaUrut);

// Tanpa mutasi: array_map() SELALU mengembalikan array baru.
$hargaPromo = array_map(fn (int $harga): float => $harga * 0.9, $hargaProduk);

echo 'Asli:  ' . implode(', ', $hargaProduk) . "\n";
echo 'Urut:  ' . implode(', ', $hargaUrut) . "\n";
echo 'Promo: ' . implode(', ', $hargaPromo) . "\n";

Output yang Dihasilkan:

text
Asli:  85000, 25000, 45000
Urut:  25000, 45000, 85000
Promo: 76500, 22500, 40500

Baris $hargaUrut = $hargaProduk; sort($hargaUrut); adalah pola yang lazim ketika kita memang butuh versi terurut sambil mempertahankan urutan asli: salin dulu, lalu urutkan salinannya. Sebaliknya, array_map() tidak pernah menyentuh $hargaProduk — fungsi tersebut membangun array ketiga ($hargaPromo) dari hasil callback. Setelah ketiga baris echo, $hargaProduk masih utuh persis seperti awal, dan itulah mengapa pipeline fungsional terasa aman: setiap tahap hanya membaca tahap sebelumnya.

Menambah Elemen Tanpa Mutasi #

Operasi paling dasar yang biasanya memaksa mutasi adalah menambah elemen. Splat operator (...) memberi kita bentuk tanpa mutasi:

php
<?php

$timLama = ['Budi', 'Siti'];

// Salinan baru dengan satu anggota tambahan — array lama tak tersentuh.
$timBaru = [...$timLama, 'Andi'];

// Elemen baru juga bisa disisipkan di depan.
$timDenganLeader = ['Zaki', ...$timLama];

echo implode(', ', $timLama) . "\n";
echo implode(', ', $timBaru) . "\n";
echo implode(', ', $timDenganLeader) . "\n";

Output yang Dihasilkan:

text
Budi, Siti
Budi, Siti, Andi
Zaki, Budi, Siti

Ekspresi [...$timLama, 'Andi'] membangun array segar yang isinya seluruh anggota lama plus satu tambahan, tanpa satu pun pernyataan penugasan pada $timLama. Mekanika lengkap unpacking array dibahas pada Chapter Array Unpacking ; pada konteks fungsional, pola ini berperan sebagai “tambah elemen” versi immutabel, padanan $arr[] = $x yang tidak menimpa apa pun.

Membekukan Struktur dengan readonly #

Array tidak bisa dikunci di PHP — selalu ada $arr['kunci'] = ... yang bisa mengubahnya. Struktur data yang benar-benar beku justru hidup di sisi object, lewat readonly class sejak PHP 8.2:

php
<?php

readonly class HargaProduk
{
    public function __construct(
        public float $nilai,
        public string $mataUang,
    ) {}

    // "Wither": mengembalikan instance baru, bukan mengubah diri sendiri.
    public function denganDiskon(float $persen): self
    {
        return new self($this->nilai * (100 - $persen) / 100, $this->mataUang);
    }
}

$hargaAsli = new HargaProduk(200000, 'IDR');
$hargaPromo = $hargaAsli->denganDiskon(25);

echo $hargaAsli->mataUang . ' ' . $hargaAsli->nilai . "\n";
echo $hargaPromo->mataUang . ' ' . $hargaPromo->nilai . "\n";

Output yang Dihasilkan:

text
IDR 200000
IDR 150000

Kunci contoh ini ada pada method denganDiskon(). Alih-alih menulis $this->nilai = ... (yang mustahil pada readonly), method tersebut membangun dan mengembalikan instance baru — pola yang dikenal sebagai wither. Setelah pemanggilan, $hargaAsli dan $hargaPromo berdampingan sebagai dua nilai mandiri: yang satu 200000, yang lain 150000. Pemanggil bebas memakai salah satunya tanpa takut yang satunya lagi ikut berubah.

Penegakan readonly sendiri tidak tanggung-tanggung:

php
<?php

readonly class SaldoToko
{
    public function __construct(public int $nilai) {}
}

$saldo = new SaldoToko(500000);
$saldo->nilai = 0;

Baris terakhir memicu Fatal error: Uncaught Error: Cannot modify readonly property SaldoToko::$nilai — PHP menolak keras penulisan dari luar constructor, jadi kebekuan struktur ini dijamin oleh runtime, bukan sekadar kesepakatan tim.

Ringkasan #

Immutability di PHP dibangun dari tiga lapis: penugasan array yang menyalin (copy-on-write), fungsi seperti array_map() yang selalu mengembalikan array baru, dan readonly class untuk struktur yang dibekukan runtime. Pola wither menggantikan mutasi object dengan pembuatan instance baru. Data yang tidak berubah dan fungsi yang tidak menimbulkan efek samping adalah fondasi gaya fungsional — tinggal satu bahan lagi yang perlu kita miliki: kemampuan memperlakukan fungsi itu sendiri sebagai data, pembahasan higher-order function pada chapter berikutnya.

Referensi #