Chapter sebelumnya memperlihatkan bahwa assignment seperti $c = $a tidak menyalin object, melainkan hanya menambah variabel yang menampung object tersebut. Lalu bagaimana kalau kita benar-benar butuh salinan yang terpisah — misalnya menyimpan kondisi keranjang sebelum diskon diterapkan, atau menyalin dokumen untuk diedit tanpa merusak versi asli? PHP menyediakan keyword clone untuk kebutuhan itu. Sayangnya clone punya satu kejutan klasik: salinannya dangkal (shallow copy), sehingga object yang bersarang di dalam property masih dibagi bersama object asli. Pada chapter ini kita akan membuktikan perilaku tersebut, memperbaikinya dengan magic method __clone, lalu mengenal alternatif yang lebih aman: object immutable.
Assignment Hanya Menyalin Handle Object #
Mari kita mulai dari perilaku dasar yang menjadi akar masalahnya. Variabel di PHP tidak menyimpan object itu sendiri, melainkan penunjuk (handle) ke object yang hidup di memori. Ketika sebuah variabel di-assign ke variabel lain, yang disalin hanyalah handle-nya — kedua variabel berakhir menampung object yang sama:
Diagram di atas adalah keadaan setelah baris $keranjangB = $keranjangA; dijalankan. Bukti nyatanya tampak pada contoh berikut:
<?php
class Keranjang
{
public array $barang = [];
}
$keranjangA = new Keranjang();
$keranjangB = $keranjangA; // hanya menyalin handle
$keranjangB->barang[] = "Kopi Torabika";
echo "A: " . implode(", ", $keranjangA->barang) . "\n";
echo "B: " . implode(", ", $keranjangB->barang) . "\n";
var_dump($keranjangA === $keranjangB);Output yang Dihasilkan:
A: Kopi Torabika
B: Kopi Torabika
bool(true)Perubahan yang dilakukan lewat $keranjangB ikut terlihat dari $keranjangA karena keduanya menampung satu object yang sama — itulah mengapa $keranjangA === $keranjangB bernilai true. Bandingkan dengan array yang merupakan value type: PHP menerapkan copy-on-write, sehingga array hasil assignment menjadi salinan terpisah begitu salah satunya diubah:
<?php
$daftarA = ["Kopi", "Teh"];
$daftarB = $daftarA; // array: salinan nilai
$daftarB[] = "Susu";
echo "A: " . implode(", ", $daftarA) . "\n";
echo "B: " . implode(", ", $daftarB) . "\n";Output yang Dihasilkan:
A: Kopi, Teh
B: Kopi, Teh, SusuKali ini $daftarA tetap berisi dua item sementara $daftarB bertambah satu, karena penambahan elemen memaksa PHP membuat salinan array terpisah terlebih dahulu. Perbedaan perilaku ini dirangkum pada tabel berikut:
| Pernyataan | Pada object | Pada array |
|---|---|---|
$b = $a; | $b menampung object yang sama dengan $a | $b menjadi salinan nilai $a |
Mengubah isi lewat $b | Terlihat juga dari $a | Tidak menyentuh $a |
$a === $b | true (identitas sama) | Bisa true karena isinya memang sama |
clone Menghasilkan Salinan Dangkal #
Keyword clone menciptakan object baru dengan seluruh property instance disalin dari object asli — constructor tidak dijalankan ulang. Namun salinan tersebut hanya setinggi satu level: jika property-nya berupa object lain, yang disalin hanyalah handle ke object bersarang itu, sehingga object asli dan salinan tetap berbagi:
Pada diagram di atas, clone berhasil membuat object Pelanggan kedua, tetapi object Alamat di dalamnya masih satu dan dibagi bersama. Mari kita buktikan lewat kode:
<?php
class Alamat
{
public function __construct(public string $kota)
{
}
}
class Pelanggan
{
public function __construct(
public string $nama,
public Alamat $alamat,
) {
}
}
$asli = new Pelanggan("Budi", new Alamat("Bandung"));
$salinan = clone $asli;
var_dump($asli === $salinan);
var_dump($asli == $salinan);
$salinan->alamat->kota = "Surabaya";
echo "Asli: {$asli->alamat->kota}\n";
echo "Salinan: {$salinan->alamat->kota}\n";Output yang Dihasilkan:
bool(false)
bool(true)
Asli: Surabaya
Salinan: SurabayaMari kita bedah hasilnya satu per satu:
$asli === $salinanbernilaifalse:clonesungguh menciptakan objectPelangganyang baru, bukan sekadar handle tambahan.$asli == $salinanbernilaitrue: class sama dan seluruh property nilainya sama — aturan perbandingan==dari chapter Object Comparison .- Kota asli ikut berubah menjadi
"Surabaya":$salinan->alamatdan$asli->alamatmenampung objectAlamatyang sama, sehingga mutasi lewat salinan merusak object asli.
Deep Copy dengan __clone #
PHP menyediakan hook khusus untuk mengendalikan proses penyalinan: magic method __clone(). Method ini dijalankan pada object salinan setelah seluruh property selesai disalin dangkal, sehingga kita berkesempatan mengganti property bersarang dengan salinannya sendiri:
<?php
class Alamat
{
public function __construct(public string $kota)
{
}
}
class Pelanggan
{
public function __construct(
public string $nama,
public Alamat $alamat,
) {
}
public function __clone(): void
{
// salin juga object di dalamnya (deep copy)
$this->alamat = clone $this->alamat;
}
}
$asli = new Pelanggan("Budi", new Alamat("Bandung"));
$salinan = clone $asli;
$salinan->alamat->kota = "Surabaya";
echo "Asli: {$asli->alamat->kota}\n";
echo "Salinan: {$salinan->alamat->kota}\n";Output yang Dihasilkan:
Asli: Bandung
Salinan: SurabayaSatu-satunya perubahan dari contoh sebelumnya adalah kehadiran __clone() yang menyalin ulang property alamat. Setelah itu, mutasi kota lewat $salinan tidak lagi menyentuh $asli — keduanya kini memegang object Alamat yang berbeda. Prinsip praktisnya: di dalam __clone(), cukup clone property yang benar-benar perlu dipisahkan; menyalin seluruh object graph secara rekursif mahal dan sering tidak diperlukan.
Perbedaan clone dan new #
clone dan new sama-sama menghasilkan object, tetapi titik berangkatnya berbeda. Constructor yang dipanggil new selalu mulai dari nol sesuai definisinya, sedangkan clone menyalin kondisi object saat ini — termasuk semua perubahan yang terjadi setelah constructor selesai:
<?php
class Dokumen
{
public function __construct(public string $judul)
{
}
}
$dokumen = new Dokumen("Draf Awal");
$dokumen->judul = "Draf Revisi 2"; // state berubah setelah constructor
$salinan = clone $dokumen; // menyalin state terkini
$dokumenBaru = new Dokumen("Draf Awal"); // constructor berjalan dari awal
echo "Salinan: {$salinan->judul}\n";
echo "Baru: {$dokumenBaru->judul}\n";Output yang Dihasilkan:
Salinan: Draf Revisi 2
Baru: Draf AwalObject hasil clone membawa judul "Draf Revisi 2" karena itulah isi object saat disalin, sementara object dari new kembali ke "Draf Awal" sesuai argumen constructor. Gunakan clone ketika yang dibutuhkan adalah potret kondisi terkini (misalnya draft untuk pratinjau), dan gunakan new ketika yang dibutuhkan adalah object segar dengan nilai awal yang bersih.
Alternatif: Object Immutable #
Ada cara ketiga yang menghilangkan seluruh masalah berbagi state: jangan mengubah object sama sekali. Object immutable menyimpan nilai dalam property readonly yang tidak bisa ditulis ulang; setiap “perubahan” justru menghasilkan object baru. Pola ini sudah akrab lewat class bawaan DateTimeImmutable:
<?php
final class Uang
{
public function __construct(
public readonly int $jumlah,
public readonly string $mataUang = "IDR",
) {
}
public function tambah(int $nominal): self
{
return new self($this->jumlah + $nominal, $this->mataUang);
}
}
$hargaAwal = new Uang(50000);
$total = $hargaAwal->tambah(25000);
echo "Awal: Rp{$hargaAwal->jumlah}\n";
echo "Total: Rp{$total->jumlah}\n";Output yang Dihasilkan:
Awal: Rp50000
Total: Rp75000Method tambah() tidak menyentuh $hargaAwal sama sekali — method tersebut membangun dan mengembalikan object Uang yang baru. Tidak ada mutasi, tidak ada object bersarang yang dibagi diam-diam, sehingga clone dan __clone() pun menjadi tidak diperlukan. Penanda final dan readonly juga menutup pintu bagi subclass atau kode lain yang mencoba melanggar janji kekebalan ini.
Ringkasan #
Assignment hanya menyalin handle sehingga dua variabel menampung satu object; clone menciptakan object baru dengan salinan property dangkal, dan property berupa object tetap dibagi sampai __clone() menyalinnya secara eksplisit. Constructor tidak berjalan saat clone, jadi hasilnya adalah potret state terkini, bukan nilai awal. Untuk data yang tidak harus berubah, object immutable dengan property readonly menghapus seluruh kelas masalah ini. __clone() sendiri sebenarnya adalah anggota dari keluarga besar magic method — dan itulah topik chapter berikutnya.