Setiap kali aplikasi berjalan, pertanyaan sederhana muncul berulang-ulang: apakah pesanan ini sama dengan pesanan itu? Apakah pengguna yang sedang login adalah pemilik keranjang ini? PHP menjawab pertanyaan semacam itu dengan dua operator yang bekerja sangat berbeda pada object: == membandingkan isi, sedangkan === membandingkan identitas. Perbedaan ini kerap mengejutkan pemula karena pada integer atau string kedua operator hampir selalu sepakat. Pada chapter ini kita akan membedah aturan main keduanya, menelusuri jebakan property yang berupa object, lalu menutup dengan cara membangun perbandingan kustom ketika PHP tidak mengerti aturan bisnis kita.

Perbedaan == dan === pada Object #

Operator == menyatakan dua object setara apabila keduanya merupakan instance dari class yang sama dan seluruh property-nya bernilai sama — perbandingan property dilakukan secara rekursif memakai == juga. Operator === jauh lebih ketat: kedua variabel harus menunjuk ke object yang sama persis, satu object yang hidup di satu tempat di memori. Ibaratnya dua cangkir kopi Torabika dari pabrik yang sama: bagi == keduanya setara, bagi === keduanya tetap dua cangkir berbeda. Posisi tiga variabel pada contoh di bawah dapat digambarkan seperti ini:

Diagram tersebut menunjukkan bahwa $a dan $c menampung satu object yang sama, sementara $b menampung object terpisah. Mari kita buktikan lewat kode:

php
<?php

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

$a = new Produk("Kopi Torabika", 25000);
$b = new Produk("Kopi Torabika", 25000);
$c = $a;

var_dump($a == $b);
var_dump($a === $b);
var_dump($a === $c);

Output yang Dihasilkan:

text
bool(true)
bool(false)
bool(true)

Mari kita bedah hasil eksekusinya:

  1. $a dan $b: dua panggilan new yang terpisah menghasilkan dua object berbeda, meskipun isi property-nya sama persis.
  2. $c = $a: assignment tidak membuat object baru; variabel $c hanya ikut menampung object yang sama dengan $a (perilaku ini kita bedah tuntas di chapter Cloning Objects ).
  3. $a == $b menghasilkan true: class-nya sama dan property nama serta harga bernilai sama.
  4. $a === $b menghasilkan false: dua instance yang berbeda tidak mungkin identik, seberapa pun mirip isinya.
  5. $a === $c menghasilkan true: keduanya menampung object yang sama.

Rangkuman ketiganya dalam satu tabel:

EkspresiHasilAlasan
$a == $btrueClass sama, seluruh property bernilai sama
$a === $bfalseDua instance yang berbeda
$a === $ctrueKeduanya menampung object yang sama

Class yang Berbeda Selalu Tidak Setara #

Syarat “instance dari class yang sama” membawa konsekuensi tegas: dua object dari class yang berbeda tidak akan pernah dianggap setara oleh ==, sekalipun property-nya kembar identik. Hal ini masuk akal karena class adalah bagian dari identitas nilai object — dua struktur data yang kebetulan sama isinya tetap mewakili konsep berbeda. Perhatikan dua metode pembayaran berikut:

php
<?php

class PembayaranQris
{
    public function __construct(public int $jumlah)
    {
    }
}

class PembayaranTunai
{
    public function __construct(public int $jumlah)
    {
    }
}

$qris = new PembayaranQris(100000);
$tunai = new PembayaranTunai(100000);

var_dump($qris == $tunai);
var_dump($qris != $tunai);

Output yang Dihasilkan:

text
bool(false)
bool(true)

Meskipun keduanya menyimpan 100000, ekspresi $qris == $tunai menghasilkan false karena PembayaranQris dan PembayaranTunai adalah class yang berbeda, sehingga $qris != $tunai bernilai true. Jika kode kita hanya ingin tahu “berapa jumlah pembayarannya”, bandingkan property-nya ($qris->jumlah === $tunai->jumlah), bukan object-nya.

Property yang Berupa Object Dibandingkan Secara Rekursif #

Aturan == menjadi menarik ketika sebuah property juga berupa object: PHP menyelam ke dalam property tersebut dan membandingkannya secara rekursif. Artinya, hasil == adalah potongan kondisi pada saat perbandingan dilakukan — begitu object bersarang berubah, hasil yang sama tidak dijamin lagi:

php
<?php

class Alamat
{
    public function __construct(public string $kota)
    {
    }
}

class Pelanggan
{
    public function __construct(
        public string $nama,
        public Alamat $alamat,
    ) {
    }
}

$pelanggan1 = new Pelanggan("Budi", new Alamat("Bandung"));
$pelanggan2 = new Pelanggan("Budi", new Alamat("Bandung"));

var_dump($pelanggan1 == $pelanggan2);
var_dump($pelanggan1 === $pelanggan2);

$pelanggan1->alamat->kota = "Surabaya";
var_dump($pelanggan1 == $pelanggan2);

Output yang Dihasilkan:

text
bool(true)
bool(false)
bool(false)

Mari kita uraikan apa yang terjadi:

  1. $pelanggan1 == $pelanggan2 awalnya true: nama sama, lalu property alamat yang berupa object dibandingkan rekursif — keduanya Alamat dengan kota "Bandung", jadi setara.
  2. $pelanggan1 === $pelanggan2 bernilai false: dua instance Pelanggan yang berbeda, apa pun isinya.
  3. Setelah mutasi, == berubah menjadi false: kota pada $pelanggan1 kini "Surabaya" sedangkan $pelanggan2 masih "Bandung", sehingga perbandingan rekursif menemukan perbedaan.

Identitas Object dengan spl_object_id #

Setiap object yang diciptakan runtime PHP membawa nomor identitas internal. Operator === pada dasarnya memeriksa apakah dua variabel menampung object dengan identitas yang sama, dan fungsi spl_object_id() memperlihatkan nomor tersebut secara langsung. Nomor ini cocok untuk dibandingkan, tetapi jangan dicetak mentah-mentah — nilainya bisa berbeda antar eksekusi dan bisa dipakai ulang setelah object lama dihancurkan garbage collector:

php
<?php

class Item
{
    public function __construct(public string $nama)
    {
    }
}

$buku1 = new Item("Buku");
$buku2 = new Item("Buku");

var_dump(spl_object_id($buku1) === spl_object_id($buku1));
var_dump(spl_object_id($buku1) === spl_object_id($buku2));
var_dump($buku1 === $buku2);

Output yang Dihasilkan:

text
bool(true)
bool(false)
bool(false)

Membandingkan spl_object_id() milik variabel yang sama menghasilkan true, sementara dua object berbeda menghasilkan false — persis selaras dengan hasil ===. Dalam kode aplikasi sehari-hari kita cukup memakai ===; fungsi spl_object_id() umumnya baru muncul di kode infrastruktur seperti cache yang menyimpan data per-object.

Perbandingan Kustom dengan <=> dan usort #

PHP tidak pernah tahu aturan bisnis kita: apakah daftar transaksi diurutkan berdasarkan total, tanggal, atau nama pelanggan? Untuk itu kita menentukan sendiri urutannya lewat spaceship operator <=> yang membandingkan dua nilai dan mengembalikan -1, 0, atau 1:

php
<?php

var_dump(1 <=> 2);
var_dump(2 <=> 2);
var_dump(3 <=> 2);

Output yang Dihasilkan:

text
int(-1)
int(0)
int(1)

Nilai -1 berarti sisi kiri lebih kecil, 0 berarti sama, dan 1 berarti sisi kiri lebih besar — tepat seperti konvensi comparator yang dibutuhkan usort(). Dengan begitu, mengurutkan daftar object berdasarkan satu property menjadi ringkas:

php
<?php

class Transaksi
{
    public function __construct(
        public string $id,
        public int $total,
    ) {
    }
}

$daftar = [
    new Transaksi("TRX-3", 750000),
    new Transaksi("TRX-1", 1500000),
    new Transaksi("TRX-2", 500000),
];

usort($daftar, fn(Transaksi $x, Transaksi $y) => $x->total <=> $y->total);

foreach ($daftar as $transaksi) {
    echo "{$transaksi->id}: Rp{$transaksi->total}\n";
}

Output yang Dihasilkan:

text
TRX-2: Rp500000
TRX-3: Rp750000
TRX-1: Rp1500000

Closure yang dikirim ke usort() menerima dua object Transaksi, lalu spaceship operator membandingkan property total keduanya; hasil -1, 0, atau 1 dipakai usort() untuk menyusun array dari total terkecil. Ketika satu property bisa seri dan kita ingin urutan kedua, rangkai dua spaceship dengan operator ?: — misalnya ($x->total <=> $y->total) ?: ($x->id <=> $y->id) — sehingga transaksi dengan total sama diurutkan lagi berdasarkan id.

Ringkasan #

Operator == membandingkan struktur: class harus sama dan seluruh property instance dibandingkan rekursif, termasuk property yang berupa object. Operator === membandingkan identitas: kedua variabel harus menampung object yang sama, konsep yang diwakili nomor internal dari spl_object_id(). Class yang berbeda tidak akan pernah setara, property static diabaikan, dan urutan bisnis dibangun sendiri lewat spaceship operator di dalam usort(). Pertanyaan berikutnya: bagaimana membuat salinan object yang benar-benar terpisah, bukan sekadar variabel lain yang menampung object yang sama? Jawabannya adalah clone, topik chapter Cloning Objects .

Referensi #