Method dan property static diwariskan dari parent ke child sama seperti member instance — namun ada satu perbedaan halus yang kerap menjebak: referensi self:: di dalam kode parent selalu menunjuk ke class tempat kode itu ditulis, bukan class yang memanggil. Akibatnya, static method warisan yang memakai new self() akan terus melahirkan object parent meski dipanggil dari class anak. Fitur late static binding (LSB) hadir untuk memperbaikinya: dengan keyword static::, resolusi class ditunda hingga runtime dan mengikuti class yang benar-benar dipanggil. Pada chapter ini kita akan membuktikan masalahnya, membedah solusinya, lalu melihat pola factory dan registry yang menjadikan LSB salah satu fitur OOP PHP paling berdaya guna.
Masalah: self Terikat Terlalu Awal #
Mari kita mulai dari bug klasiknya. Class Pengguna menyediakan static method buatBaru() sebagai pintu resmi menciptakan instance — pola yang lazim disebut static factory. Class Admin mewarisinya tanpa mengubah apa pun:
<?php
class Pengguna
{
public function __construct(public string $nama)
{
}
public static function buatBaru(string $nama): Pengguna
{
return new self($nama);
}
}
class Admin extends Pengguna
{
}
$admin = Admin::buatBaru("Budi");
var_dump($admin instanceof Pengguna);
var_dump($admin instanceof Admin);
echo get_class($admin) . "\n";Output yang Dihasilkan:
bool(true)
bool(false)
PenggunaHasilnya mengejutkan: $admin instanceof Admin bernilai false dan get_class() melaporkan "Pengguna". Penyebabnya ada pada new self($nama) — ekspresi self:: diresolve PHP menggunakan informasi dari class tempat baris kode tersebut ditulis, yaitu Pengguna, terlepas dari siapa yang memanggil. Kata Admin::buatBaru("Budi") memang meminjam method warisan, tetapi isi method itu tetap berpegang pada class perumusnya. Perilaku ini dinamakan early binding: keputusan sudah diambil sejak kode berada di parent.
static:: Menunda Resolusi hingga Runtime #
Late static binding memindahkan momen keputusan itu ke saat eksekusi. Ketika PHP bertemu static::, PHP menanyakan dulu “class mana yang sedang dipanggil?” — informasi yang bisa diambil lewat get_called_class() — baru menentukan tujuan referensinya. Perbandingan keduanya paling jelas saat berdampingan dalam satu hierarchy:
<?php
class Hewan
{
public static function panggilSelf(): string
{
return self::suara();
}
public static function panggilStatic(): string
{
return static::suara();
}
public static function suara(): string
{
return "suara hewan umum";
}
}
class Kucing extends Hewan
{
public static function suara(): string
{
return "meong";
}
}
echo "Hewan::panggilSelf(): " . Hewan::panggilSelf() . "\n";
echo "Kucing::panggilSelf(): " . Kucing::panggilSelf() . "\n";
echo "Hewan::panggilStatic(): " . Hewan::panggilStatic() . "\n";
echo "Kucing::panggilStatic(): " . Kucing::panggilStatic() . "\n";Output yang Dihasilkan:
Hewan::panggilSelf(): suara hewan umum
Kucing::panggilSelf(): suara hewan umum
Hewan::panggilStatic(): suara hewan umum
Kucing::panggilStatic(): meongMari kita baca hasilnya baris demi baris:
Hewan::panggilSelf()danHewan::panggilStatic(): pemanggilnyaHewan, jadi kedua referensi sama-sama mendarat diHewan::suara()— hasil"suara hewan umum".Kucing::panggilSelf():self::tetap terikat ke class perumus kode, yakniHewan, sehingga hasilnya masih"suara hewan umum"meskiKucingmenimpasuara().Kucing::panggilStatic():static::diresolve saat runtime terhadap class pemanggil,Kucing, sehingga yang terpanggil adalahKucing::suara()dan hasilnya"meong".
Referensi di dalam Hewan | Dipanggil sebagai Hewan:: | Dipanggil sebagai Kucing:: |
|---|---|---|
self::suara() | Hewan::suara() | masih Hewan::suara() |
static::suara() | Hewan::suara() | Kucing::suara() |
new static() sebagai Pola Factory #
Kembali ke bug buatBaru() di awal chapter: solusinya adalah mengganti new self() dengan new static(). Return type static (tersedia sejak PHP 8) melengkapi pola ini dengan menjamin tanda tangan method tetap jujur tentang jenis object yang dikembalikan:
<?php
class Pengguna
{
public function __construct(public string $nama)
{
}
public static function buatBaru(string $nama): static
{
return new static($nama);
}
}
class Admin extends Pengguna
{
}
$admin = Admin::buatBaru("Budi");
var_dump($admin instanceof Admin);
echo get_class($admin) . "\n";Output yang Dihasilkan:
bool(true)
AdminKali ini $admin instanceof Admin bernilai true dan get_class() melaporkan "Admin" karena new static() menunggu runtime: siapa pemanggilnya, dialah class yang diinstansiasi. Return type static membuat Admin::buatBaru() dipahami sebagai penghasil object Admin — informasi yang sangat dihargai static analysis dan IDE. Return type ini bersifat kovarian secara otomatis, jadi tiap subclass mewarisi method factory yang sekaligus “berjanji” menghasilkan dirinya sendiri.
Studi Kasus: Registry Per Subclass #
Pola klasik yang mengandalkan LSB adalah registry per jenis domain object: setiap subclass menyimpan catatan instance miliknya sendiri, dan method penghitungnya tinggal satu di parent. Kuncinya ada dua: static::$tersimpan untuk mengarahkan penyimpanan ke class yang benar, dan redeklarasi property static di tiap subclass agar masing-masing punya penyimpanan terpisah:
<?php
abstract class Entitas
{
protected static array $tersimpan = [];
public static function register(self $entitas): void
{
static::$tersimpan[] = $entitas;
}
public static function hitung(): int
{
return count(static::$tersimpan);
}
}
class Pengguna extends Entitas
{
protected static array $tersimpan = [];
}
class Pesanan extends Entitas
{
protected static array $tersimpan = [];
}
Pengguna::register(new Pengguna());
Pengguna::register(new Pengguna());
Pesanan::register(new Pesanan());
echo "Pengguna terdaftar: " . Pengguna::hitung() . "\n";
echo "Pesanan terdaftar: " . Pesanan::hitung() . "\n";
echo "Entitas terdaftar: " . Entitas::hitung() . "\n";Output yang Dihasilkan:
Pengguna terdaftar: 2
Pesanan terdaftar: 1
Entitas terdaftar: 0Mari kita bedah kerja samanya:
register()danhitung()hanya ditulis sekali diEntitas, tetapistatic::$tersimpanmembuat PHP memakai penyimpanan milik class pemanggil —Pengguna::register()mengisi array milikPengguna.- Redeklarasi
protected static array $tersimpan = []di setiap subclass adalah syarat mutlak: tanpa redeklarasi, seluruh subclass berbagi satu property warisan sehingga catatan pengguna dan pesanan tercampur jadi satu. Entitas::hitung()bernilai0: class induk memiliki array sendiri yang memang tidak pernah diisi, membuktikan ketiga penyimpanan benar-benar terpisah.
static dalam Trait #
Trait menambahkan nuansa menarik karena anggotanya disalin ke setiap class pemakai. Property static yang dideklarasikan di dalam trait tidak dibagi antar class pemakai — masing-masing class mendapat salinannya sendiri — dan static:: di dalam method trait menunjuk ke class yang mengomposisinya. Kombinasi ini membuat trait penghitung instance bekerja mandiri per class:
<?php
trait BisaDihitung
{
private static int $jumlah = 0;
public function __construct()
{
static::$jumlah++;
}
public static function jumlahDibuat(): int
{
return static::$jumlah;
}
}
class Berita
{
use BisaDihitung;
}
class Artikel
{
use BisaDihitung;
}
new Berita();
new Berita();
new Artikel();
echo "Berita: " . Berita::jumlahDibuat() . "\n";
echo "Artikel: " . Artikel::jumlahDibuat() . "\n";Output yang Dihasilkan:
Berita: 2
Artikel: 1Constructor dari trait menaikkan penghitung lewat static::$jumlah++, dan karena Berita serta Artikel masing-masing memegang salinan property sendiri, hasilnya 2 dan 1 — bukan 3 berbagi. Trait semacam ini adalah cara ringkas menyuntikkan perilaku berbasis identitas class ke banyak class sekaligus, selaras dengan apa yang kita pelajari di chapter Traits
.
Ringkasan #
self:: terikat ke class tempat kode ditulis, sementara static:: menunda resolusinya hingga runtime dan mengikuti class pemanggil — itulah inti late static binding. Pola new static() memperbaiki static factory agar subclass melahirkan dirinya sendiri, pasangan static::$properti dengan redeklarasi memungkinkan penyimpanan per subclass, dan trait dapat berperilaku independen di tiap class pemakainya. Dengan ini kotak peralatan OOP dasar PHP Anda sudah lengkap. Chapter penutup section ini mengambil jeda dari fitur dan mengajukan pertanyaan yang lebih penting: kapan sebaiknya semua peralatan ini dipakai, dan kapan tidak.