Setiap kali method di-override atau interface diimplementasikan, satu pertanyaan struktural selalu muncul: bolehkah deklarasi tipe berubah dibanding parent-nya? Variance adalah nama untuk aturan arah perubahan tersebut. Ketiga jenisnya — covariance, contravariance, dan invariance — menentukan tipe mana yang boleh menyempit, memperluas, atau wajib tetap identik. Chapter ini membangun kerangka konsepnya secara sistematis, mulai dari alasan di balik tiap aturan, demo inheritance yang dapat diuji, hingga tentative return types pada class bawaan PHP.
Tiga Jenis Variance #
Ketika class anak mengubah deklarasi tipe milik class parent, perubahan tersebut jatuh ke salah satu dari tiga kategori:
- Covariance (menyempitkan): tipe diganti dengan subtype-nya — misalnya
HewanmenjadiKucing. Anak menjanjikan sesuatu yang lebih spesifik dari parent. - Contravariance (memperluas): tipe diganti dengan supertype-nya — misalnya
KucingmenjadiHewan. Anak menerima lebih banyak dari yang parent janjikan. - Invariance (tetap identik): tipe tidak boleh berubah sama sekali. Satu-satunya pilihan adalah tipe yang persis sama dengan parent.
Jenis mana yang berlaku bergantung pada posisi tipe tersebut — parameter, return value, atau property. PHP menegakkan aturan ini dengan fatal error saat class dideklarasikan, sehingga pelanggaran tidak pernah sampai ke produksi secara diam-diam.
Mengapa Parameter Diperluas, Return Disempitkan #
Alasan kedua aturan tersebut berakar pada arah aliran nilai. Argumen mengalir masuk ke method: pemanggil menulis kode berdasarkan kontrak parent dan boleh mengirim nilai bertipe apa pun yang parent janjikan. Jika class anak menyempitkan parameter, panggilan yang sah terhadap parent akan ditolak oleh anak — substitusi rusak. Karena itu parameter hanya boleh diperluas (contravariance).
Return value mengalir keluar dari method: pemanggil mengonsumsi hasil dengan asumsi bertipe paling luas sebagaimana parent janjikan. Class anak boleh menjanjikan tipe yang lebih spesifik, karena setiap Kucing otomatis juga Hewan. Karena itu return hanya boleh disempitkan (covariance). Dua aturan ini adalah wujud teknis dari Liskov Substitution Principle — prinsip yang dibedah tuntas pada Chapter Liskov Substitution
.
Parameter Boleh Diperluas #
Contoh pertama menunjukkan contravariance yang diterima PHP. Class TokoHewan mewarisi TokoKucing dan memperluas parameter method menjual() dari Kucing menjadi Hewan:
<?php
class Hewan
{
public function perkenalan(): string
{
return "seekor hewan";
}
}
class Kucing extends Hewan
{
public function perkenalan(): string
{
return "seekor kucing";
}
}
class TokoKucing
{
public function menjual(Kucing $kucing): string
{
return "Toko kecil menjual kucing";
}
}
class TokoHewan extends TokoKucing
{
public function menjual(Hewan $hewan): string
{
return "Toko besar menjual " . $hewan->perkenalan();
}
}
echo (new TokoHewan())->menjual(new Kucing()), "\n";Output yang Dihasilkan:
Toko besar menjual seekor kucingPemanggilan menjual(new Kucing()) tetap valid karena Kucing adalah Hewan — toko besar menerima semua yang toko kecil terima, lalu lebih. Pemanggil yang memegang object TokoKucing maupun TokoHewan tidak akan pernah terkejut: argumen bertipe Kucing diterima oleh keduanya.
Parameter Tidak Boleh Disempitkan #
Arah sebaliknya langsung ditolak PHP. Class TokoKucing mencoba menyempitkan parameter menjual() dari Hewan menjadi Kucing:
<?php
class Hewan {}
class Kucing extends Hewan {}
class TokoHewan
{
public function menjual(Hewan $hewan): string
{
return "Toko menjual hewan";
}
}
class TokoKucing extends TokoHewan
{
public function menjual(Kucing $kucing): string
{
return "Toko menjual kucing";
}
}Output yang Dihasilkan:
Fatal error: Declaration of TokoKucing::menjual(Kucing $kucing): string must be compatible with TokoHewan::menjual(Hewan $hewan): string in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/variance/.verify_tmp.php on line 16Fatal error Declaration of TokoKucing::menjual(Kucing $kucing): string must be compatible with TokoHewan::menjual(Hewan $hewan): string muncul saat deklarasi class diproses. Penyempitan ini memang berbahaya: kode yang memanggil menjual(new Anjing()) melalui TokoHewan akan mendadak invalid jika diberi object TokoKucing — persis skenario yang dicegah Liskov Substitution Principle.
Return Boleh Disempitkan #
Sekarang giliran covariance. Class PabrikKucing mengoverride buat() dan menyempitkan return type dari Hewan menjadi Kucing:
<?php
abstract class Hewan
{
abstract public function suara(): string;
}
class Kucing extends Hewan
{
public function suara(): string
{
return "meong";
}
}
class PabrikHewan
{
public function buat(): Hewan
{
throw new LogicException("Belum diimplementasikan");
}
}
class PabrikKucing extends PabrikHewan
{
public function buat(): Kucing
{
return new Kucing();
}
}
$kucing = (new PabrikKucing())->buat();
echo $kucing->suara(), "\n";Output yang Dihasilkan:
meongMethod buat() pada PabrikKucing menjanjikan Kucing, yang lebih spesifik daripada janji Hewan milik parent. Janji yang lebih ketat tidak pernah mengecewakan pemanggil: siapa pun yang mengharapkan Hewan tetap puas, sementara pemanggil yang tahu sedang memakai PabrikKucing mendapat informasi tipe lebih kaya — cocok untuk operasi spesifik seperti memanggil suara() tanpa pengecekan tambahan.
Return Tidak Boleh Diperluas #
Memperluas return type justru melanggar janji parent. Class PabrikHewan mencoba melonggarkan buat() dari Kucing menjadi Hewan:
<?php
class Hewan {}
class Kucing extends Hewan {}
class PabrikKucing
{
public function buat(): Kucing
{
return new Kucing();
}
}
class PabrikHewan extends PabrikKucing
{
public function buat(): Hewan
{
return new Kucing();
}
}Output yang Dihasilkan:
Fatal error: Declaration of PabrikHewan::buat(): Hewan must be compatible with PabrikKucing::buat(): Kucing in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/variance/.verify_tmp.php on line 16PHP menghentikan program dengan pesan Declaration of PabrikHewan::buat(): Hewan must be compatible with PabrikKucing::buat(): Kucing. Pelanggaran ini merugikan pemanggil yang sudah menulis kode mengandalkan janji Kucing — misalnya memanggil method khusus kucing pada hasil buat() — lalu tiba-tiba mungkin menerima Hewan lain dari class anak.
Property Tetap Invariant #
Property menempati posisi khusus: property dapat dibaca dan ditulis melalui referensi bertipe parent, sehingga arah baca dan tulis saling bertentakan. Penyempitan menghalangi penulisan lewat parent; perluasan merusak jaminan pembacaan lewat parent. Satu-satunya pilihan aman adalah tipe identik — kita sudah membuktikannya dengan fatal error pada Chapter Property Types
. Parameter by-reference (&$nilai) menempati wilayah yang lebih halus: deklarasinya mengikuti contravariance (hanya penyempitan yang fatal), tetapi arah tulis balik referensi membuat pelebaran tipe berisiko mengejutkan — demo beserta jebakannya dibedah tuntas pada Chapter Covariance dan Contravariance
.
Tabel berikut merangkum seluruh aturan variance dalam satu pandangan:
| Posisi | Diperbolehkan | Dilarang (fatal error) |
|---|---|---|
| Parameter | memperluas: menjual(Kucing) → menjual(Hewan) | menyempitkan: menjual(Hewan) → menjual(Kucing) |
| Return | menyempitkan: buat(): Hewan → buat(): Kucing | memperluas: buat(): Kucing → buat(): Hewan |
| Property | tipe harus identik | semua perubahan, termasuk menambah ? |
| Parameter by-reference | memperluas (berisiko tulis balik) | menyempitkan atau mengganti tipe lain |
Tentative Return Types #
Aturan return covariance memiliki satu pengecualian historis: method pada class bawaan PHP (dari extension) dideklarasikan dengan tentative return type sejak PHP 8.1. Menoverride method seperti itu tanpa return type tidak memicu fatal error, melainkan Deprecated — sinyal transisi agar kode lama sempat diperbarui:
<?php
class Keranjang implements ArrayAccess
{
private array $item = [];
public function offsetExists(mixed $offset): bool
{
return isset($this->item[$offset]);
}
public function offsetGet(mixed $offset)
{
return $this->item[$offset] ?? null;
}
public function offsetSet(mixed $offset, mixed $value): void
{
$this->item[$offset] = $value;
}
public function offsetUnset(mixed $offset): void
{
unset($this->item[$offset]);
}
}
$keranjang = new Keranjang();
$keranjang["kopi"] = 2;
echo $keranjang["kopi"], "\n";Output yang Dihasilkan:
Deprecated: Return type of Keranjang::offsetGet(mixed $offset) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/variance/.verify_tmp.php on line 12
2Method offsetGet() di atas sengaja tidak mencantumkan return type, padahal ArrayAccess::offsetGet() menjanjikan mixed. PHP menerbitkan Deprecated saat class dideklarasikan, namun program tetap berjalan hingga selesai dan mencetak 2. Untuk menunda perbaikan secara sadar, tambahkan attribute #[\ReturnTypeWillChange] pada method yang bersangkutan:
<?php
class Keranjang implements ArrayAccess
{
private array $item = [];
public function offsetExists(mixed $offset): bool
{
return isset($this->item[$offset]);
}
#[\ReturnTypeWillChange]
public function offsetGet(mixed $offset)
{
return $this->item[$offset] ?? null;
}
public function offsetSet(mixed $offset, mixed $value): void
{
$this->item[$offset] = $value;
}
public function offsetUnset(mixed $offset): void
{
unset($this->item[$offset]);
}
}
$keranjang = new Keranjang();
$keranjang["gula"] = 5;
echo $keranjang["gula"], "\n";Output yang Dihasilkan:
5Attribute tersebut mematikan peringatan sementara tanpa mengubah perilaku — anggap sebagai pengakuan utang yang harus dilunasi dengan menuliskan return type mixed secara eksplisit. Detail aturan variance resminya terdokumentasi pada manual OOP PHP
, dan sejarah tentative return types tercatat pada panduan migrasi PHP 8.1
.
Ringkasan #
Variance memberi kita kerangka berpikir yang jelas: parameter aman diperluas karena nilai mengalir masuk, return aman disempitkan karena nilai mengalir keluar, dan property wajib identik karena nilai bergerak dua arah. Seluruh pelanggaran ditolak PHP sebelum eksekusi, dengan satu pengecualian transisi berupa tentative return types. Kerangka konseptual ini perlu ditempa dengan latihan konkret — Chapter Covariance dan Contravariance menyajikan studi kasus hierarchy lengkap, kombinasi keduanya dalam satu class, by-reference parameter, serta sejarah versi PHP di baliknya.