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
<?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:

text
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
<?php

$daftarA = ["Kopi", "Teh"];
$daftarB = $daftarA; // array: salinan nilai

$daftarB[] = "Susu";

echo "A: " . implode(", ", $daftarA) . "\n";
echo "B: " . implode(", ", $daftarB) . "\n";

Output yang Dihasilkan:

text
A: Kopi, Teh
B: Kopi, Teh, Susu

Kali 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:

PernyataanPada objectPada array
$b = $a;$b menampung object yang sama dengan $a$b menjadi salinan nilai $a
Mengubah isi lewat $bTerlihat juga dari $aTidak menyentuh $a
$a === $btrue (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
<?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:

text
bool(false)
bool(true)
Asli: Surabaya
Salinan: Surabaya

Mari kita bedah hasilnya satu per satu:

  1. $asli === $salinan bernilai false: clone sungguh menciptakan object Pelanggan yang baru, bukan sekadar handle tambahan.
  2. $asli == $salinan bernilai true: class sama dan seluruh property nilainya sama — aturan perbandingan == dari chapter Object Comparison .
  3. Kota asli ikut berubah menjadi "Surabaya": $salinan->alamat dan $asli->alamat menampung object Alamat yang 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
<?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:

text
Asli: Bandung
Salinan: Surabaya

Satu-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
<?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:

text
Salinan: Draf Revisi 2
Baru: Draf Awal

Object 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
<?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:

text
Awal: Rp50000
Total: Rp75000

Method 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.

Referensi #