Chapter sebelumnya sudah memperkenalkan satu magic method: __clone() yang dijalankan PHP ketika object disalin. PHP sebenarnya menyediakan satu keluarga penuh method bernama serupa — semua diawali dua garis bawah — yang tidak pernah kita panggil secara langsung. Runtime PHP-lah yang memanggil method-method tersebut pada momen-momen khusus: saat property tak dikenal dibaca, saat method tak dikenal dipanggil, saat object dipaksa menjadi string, hingga saat object di-serialize. Dengan memanfaatkan hook ini, object kita bisa terasa “hidup” mengikuti gaya bahasa PHP sendiri. Pada chapter ini kita akan menelusuri magic method yang paling sering dipakai, lalu membahas dengan jujur kapan keajaiban semacam ini justru menjadi jebakan.

Apa Itu Magic Method #

Magic method adalah method bernama khusus yang didefinisikan di dalam class untuk mengubah cara PHP menanggil operasi bahasa tertentu pada object tersebut. Nama-namanya dipesan oleh PHP, tidak boleh dipakai untuk keperluan lain, dan konvensinya tidak pernah dipanggil manual — misalnya kita menulis $objek->apa() alih-alih $objek->__call(). Dua di antaranya sudah Anda kenal: __construct() dari chapter Constructors dan __clone() dari chapter Cloning Objects . Tabel berikut memetakan anggota keluarga yang akan kita bahas:

Magic methodDijalankan ketika
__get(), __set()Membaca / menulis property yang tidak ada atau tidak terlihat
__isset(), __unset()isset() / unset() pada property semacam itu
__call(), __callStatic()Memanggil method yang tidak ada atau tidak terlihat
__toString()Object dipaksa menjadi string
__invoke()Object dipanggil layaknya function
__debugInfo()var_dump() menampilkan object
__sleep(), __wakeup()serialize() / unserialize() pada object

Property Overloading: __get, __set, __isset, __unset #

Ketika sebuah property tidak dideklarasikan atau tidak terlihat dari tempat pengaksesan, PHP tidak langsung menyerah — PHP memeriksa apakah class menyediakan __get() atau __set(). Mekanisme ini disebut property overloading: property seolah-olah tercipta secara dinamis, padahal implementasinya kita kendalikan sepenuhnya. Contoh klasiknya adalah penyimpanan konfigurasi yang menerima nama pengaturan apa pun:

php
<?php

class PenyimpananKonfigurasi
{
    private array $data = [];

    public function __set(string $nama, mixed $nilai): void
    {
        $this->data[$nama] = $nilai;
    }

    public function __get(string $nama): mixed
    {
        if (!array_key_exists($nama, $this->data)) {
            throw new RuntimeException("Konfigurasi \"$nama\" tidak ditemukan");
        }
        return $this->data[$nama];
    }

    public function __isset(string $nama): bool
    {
        return isset($this->data[$nama]);
    }

    public function __unset(string $nama): void
    {
        unset($this->data[$nama]);
    }
}

$konfigurasi = new PenyimpananKonfigurasi();
$konfigurasi->namaAplikasi = "Toko Budi";

echo $konfigurasi->namaAplikasi . "\n";
var_dump(isset($konfigurasi->namaAplikasi));
var_dump(isset($konfigurasi->zonaWaktu));

unset($konfigurasi->namaAplikasi);
var_dump(isset($konfigurasi->namaAplikasi));

Output yang Dihasilkan:

text
Toko Budi
bool(true)
bool(false)
bool(false)

Mari kita bedah alur eksekusinya:

  1. $konfigurasi->namaAplikasi = "Toko Budi": property namaAplikasi tidak dideklarasikan di class, sehingga PHP menjalankan __set("namaAplikasi", "Toko Budi") yang menyimpan nilai ke array $data.
  2. echo $konfigurasi->namaAplikasi: pembacaan property tak dikenal dialirkan ke __get(), yang mengembalikan nilai dari $data atau melempar exception untuk nama yang tidak ada.
  3. isset() dan unset(): keduanya juga punya hook masing-masing, __isset() dan __unset(), sehingga object tetap berperilaku wajar di depan isset().

Method Overloading: __call dan __callStatic #

Perilaku serupa berlaku untuk method: __call() menerima setiap pemanggilan method yang tidak dikenal beserta argumennya, sementara __callStatic() menangani versi static-nya. Kekuatan ini paling terasa saat kita ingin membangun API yang mengalir (fluent) dengan nama method yang dibentuk dari nama kolom — pola yang populer di query builder:

php
<?php

class KueriBuilder
{
    private array $kondisi = [];

    public function __call(string $nama, array $argumen): static
    {
        if (str_starts_with($nama, "dimana")) {
            $kolom = strtolower(substr($nama, 6)); // buang prefiks "dimana"
            $this->kondisi[] = $kolom . " = " . $argumen[0];
        }
        return $this;
    }

    public function sql(): string
    {
        if ($this->kondisi === []) {
            return "SELECT * FROM produk";
        }
        return "SELECT * FROM produk WHERE " . implode(" AND ", $this->kondisi);
    }
}

$kueri = (new KueriBuilder())
    ->dimanaKategori("elektronik")
    ->dimanaHarga(1500000);

echo $kueri->sql() . "\n";

Output yang Dihasilkan:

text
SELECT * FROM produk WHERE kategori = elektronik AND harga = 1500000

Method dimanaKategori() dan dimanaHarga() tidak pernah didefinisikan — keduanya ditangkap __call(), dipilah prefiksnya, lalu diterjemahkan menjadi kondisi kategori = ... dan harga = .... Karena __call() mengembalikan $this, panggilan bisa dirantai berurutan sebelum akhirnya ditutup sql() yang merangkai string kueri final. Untuk konteks static, hook-nya adalah __callStatic() yang harus dideklarasikan sebagai method static:

php
<?php

class PencatatLog
{
    public static function __callStatic(string $level, array $argumen): void
    {
        $pesan = $argumen[0] ?? "";
        echo "[" . strtoupper($level) . "] $pesan\n";
    }
}

PencatatLog::info("Server menyala");
PencatatLog::error("Koneksi database gagal");

Output yang Dihasilkan:

text
[INFO] Server menyala
[ERROR] Koneksi database gagal

Panggilan PencatatLog::info() dan PencatatLog::error() tidak menemukan method static bernama demikian, sehingga keduanya diarahkan ke __callStatic() dengan nama level sebagai argumen pertama. Satu method sederhana kini melayani level apa pun tanpa perlu menulis info(), error(), warning() satu per satu.

Object sebagai String dengan __toString #

Saat object berhadapan dengan konteks string — echo, interpolasi "$produk", atau parameter bertipe string — PHP mencari __toString() di dalam class-nya:

php
<?php

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

    public function __toString(): string
    {
        return $this->nama . " (Rp" . $this->harga . ")";
    }
}

$produk = new Produk("Kopi Torabika", 25000);
echo "Produk unggulan: $produk\n";
echo strtoupper($produk) . "\n";

Output yang Dihasilkan:

text
Produk unggulan: Kopi Torabika (Rp25000)
KOPI TORABIKA (RP25000)

Kedua baris output memperlihatkan __toString() bekerja dari dua arah: interpolasi string "$produk" dan fungsi strtoupper() yang menerima parameter string sama-sama memperoleh hasil konversinya. Method ini juga membuat log dan pesan error jadi ringkas karena object bisa dicetak langsung tanpa memanggil method formatter setiap kali.

Object sebagai Callable dengan __invoke #

Magic method __invoke() mengizinkan object dipanggil persis seperti function — cukup tulis $object($argumen). Object semacam ini tetap membawa state dan property-nya, tetapi bisa ditempatkan di mana pun PHP menerima callable:

php
<?php

class PemformatHarga
{
    public function __construct(private string $mataUang = "Rp")
    {
    }

    public function __invoke(int $nominal): string
    {
        return $this->mataUang . number_format($nominal, 0, ",", ".");
    }
}

$format = new PemformatHarga();
echo $format(1500000) . "\n";

$daftarHarga = [25000, 150000, 2750000];
$berformat = array_map($format, $daftarHarga);
echo implode(" | ", $berformat) . "\n";

Output yang Dihasilkan:

text
Rp1.500.000
Rp25.000 | Rp150.000 | Rp2.750.000

Pemanggilan $format(1500000) mengeksekusi __invoke() yang memadukan property $mataUang dengan number_format(). Karena object ini callable, ia bisa langsung dikirim ke array_map() tanpa dibungkus closure apa pun — perhatikan kemiripannya dengan konsep first-class callable pada chapter First-Class Callables . Pola ini nyaman untuk strategy yang butuh konfigurasi: konfigurasinya hidup sebagai property, perilakunya tetap bisa ditempatkan sebagai callable biasa.

Menyesuaikan var_dump dengan __debugInfo #

Saat var_dump() menampilkan object, PHP memanggil __debugInfo() bila method ini tersedia, lalu menampilkan array hasilnya sebagai pengganti daftar property mentah. Kegunaan yang paling praktis: menyembunyikan data sensitif dari layar debugging:

php
<?php

class KoneksiDatabase
{
    public function __construct(
        private string $host,
        private string $password,
    ) {
    }

    public function __debugInfo(): array
    {
        return [
            'host' => $this->host,
            'password' => '***tersembunyi***',
        ];
    }
}

$koneksi = new KoneksiDatabase("localhost", "YOUR_DATABASE_PASSWORD");
var_dump($koneksi);

Output yang Dihasilkan:

text
object(KoneksiDatabase)#1 (2) {
  ["host"]=>
  string(9) "localhost"
  ["password"]=>
  string(17) "***tersembunyi***"
}

Tanpa __debugInfo(), var_dump($koneksi) akan menampilkan password asli secara polos — bencana kecil ketika developer menempelkan output debugging ke tiket atau grup chat. Dengan hook ini, tampilan var_dump tetap informatif untuk host namun menyamarkan password. Method ini hanya memengaruhi tampilan var_dump(), bukan print_r() maupun serialize().

Serialisasi: __sleep dan __wakeup #

Dua magic method terakhir yang kita bahas bekerja pada proses serialisasi — mengubah object menjadi string yang bisa disimpan, lalu membangunnya kembali (materi lengkapnya ada di dokumentasi Serialization ). Method __sleep() mengembalikan daftar nama property yang ingin disimpan, sedangkan __wakeup() berjalan pada object hasil unserialize() untuk membangun ulang state turunan:

php
<?php

class SesiPengguna
{
    public function __construct(
        public string $pengguna,
        public DateTimeImmutable $dibuat,
    ) {
    }

    public function __sleep(): array
    {
        // simpan hanya nama pengguna
        return ['pengguna'];
    }

    public function __wakeup(): void
    {
        // bangun ulang property turunan saat dimuat
        $this->dibuat = new DateTimeImmutable(
            "2026-01-15 08:00:00",
            new DateTimeZone("Asia/Jakarta"),
        );
    }
}

$sesi = new SesiPengguna(
    "Budi",
    new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta")),
);
$tersimpan = serialize($sesi);
echo $tersimpan . "\n";

$dimuat = unserialize($tersimpan);
echo $dimuat->pengguna . " - " . $dimuat->dibuat->format("Y-m-d H:i:s") . "\n";

Output yang Dihasilkan:

text
O:12:"SesiPengguna":1:{s:8:"pengguna";s:4:"Budi";}
Budi - 2026-01-15 08:00:00

String serialize() hanya memuat satu property (pengguna) karena itulah satu-satunya nama yang dikembalikan __sleep(). Saat unserialize() membangun object kembali, __wakeup() berjalan dan mengisi ulang property dibuat yang tidak ikut disimpan. Pola ini berguna untuk memangkas data besar yang bisa direkonstruksi, atau menutup koneksi lama yang tidak bermakna lintas proses.

Kapan Magic Method Menjadi Jebakan #

Semua kekuatan di atas punya harga yang tidak tertera di faktur. Bayangkan property overloading yang terlalu murah hati mengembalikan nilai untuk nama property apa pun:

php
<?php

class KonfigurasiLonggar
{
    public function __get(string $nama): mixed
    {
        return null; // mengembalikan null untuk property apa pun
    }
}

$longgar = new KonfigurasiLonggar();
$benar = $longgar->namaAplikasi;
$salahKetik = $longgar->namaApp;

var_dump($benar);
var_dump($salahKetik);

Output yang Dihasilkan:

text
NULL
NULL

Kedua akses mengembalikan NULL — termasuk $longgar->namaApp yang sebenarnya salah ketik dari namaAplikasi. Pada class biasa, salah ketik semacam itu langsung memicu error yang jelas; di sini kesalahan itu tertelan diam-diam dan baru terasa jauh di hilir alur program. Itulah inti masalahnya.

Ringkasan #

Magic method adalah titik kait antara class kita dan operasi bahasa PHP: __get/__set/__isset/__unset untuk property dinamis, __call/__callStatic untuk method dinamis, __toString untuk konversi string, __invoke agar object menjadi callable, __debugInfo untuk tampilan var_dump(), serta __sleep/__wakeup untuk serialisasi. Kekuatan terbesarnya — perilaku yang tercipta secara diam-diam — juga merupakan risiko terbesarnya terhadap alat bantu dan keterbacaan. Salah satu hook yang telah kita lihat, __callStatic(), bekerja pada konteks static; chapter berikutnya membahas fitur yang berbagi ranah yang sama namun jauh lebih mendasar: bagaimana static:: menunda resolusi class hingga runtime.

Referensi #