Kerangka variance dari Chapter sebelumnya kini kita tempa dengan studi kasus konkret. Covariance dan contravariance bukan dua fitur terpisah, melainkan dua sisi dari satu jaminan: object class anak harus dapat menggantikan object class parent tanpa merusak kode pemanggil. Chapter ini menyajikan hierarchy class lengkap yang dapat dijalankan — penampungan hewan dengan return covariant, penjaga dengan parameter contravariant, gabungan keduanya dalam satu class, hingga kasus khusus parameter by-reference yang menyimpan jebakan tulis balik.
Covariance — Menyempitkan Return Type #
Studi kasus pertama: rantai penampungan hewan. Class Penampungan menjanjikan method adopsi() yang mengembalikan Hewan apa pun, lalu class PenampunganKucing mengoverride method tersebut dengan return type yang lebih spesifik:
<?php
abstract class Hewan
{
abstract public function suara(): string;
}
class Kucing extends Hewan
{
public function suara(): string
{
return "meong";
}
}
class Anjing extends Hewan
{
public function suara(): string
{
return "guk guk";
}
}
class Penampungan
{
public function adopsi(): Hewan
{
return new Anjing();
}
}
class PenampunganKucing extends Penampungan
{
public function adopsi(): Kucing
{
return new Kucing();
}
}
$hewan = (new PenampunganKucing())->adopsi();
echo $hewan->suara(), "\n";Output yang Dihasilkan:
meongMari kita bedah hierarchy di atas:
abstract class Hewan: akar hierarchy dengan method abstraksuara(), sehingga setiap anak wajib mendeklarasikan suaranya sendiri.Penampungan::adopsi(): Hewan: parent menjanjikan hasil adopsi bertipe paling umum —Hewan— dan implementasi bawaannya mengembalikanAnjing.PenampunganKucing::adopsi(): Kucing: override dengan return typeKucing, yang lebih spesifik daripada janji parent. Inilah covariance.$hewan->suara(): pemanggil yang tahu sedang berurusan denganPenampunganKucinglangsung menerimaKucing— termasuk seluruh method khusus kucing bila ada — tanpa pengecekaninstanceoftambahan.
Pemanggil yang hanya memegang Penampungan tetap aman: hasil Kucing otomatis memenuhi janji Hewan. Tidak ada pihak yang dirugikan oleh penyempitan ini, dan itulah alasan PHP menerimanya.
Contravariance — Memperluas Parameter #
Studi kasus kedua membalik arahnya: parameter. Class Penjaga hanya bisa memberi MakananKucing, lalu class PenjagaSerbaguna memperluas kemampuannya hingga MakananHewan apa pun:
<?php
interface MakananHewan
{
public function label(): string;
}
class MakananKucing implements MakananHewan
{
public function label(): string
{
return "makanan kucing";
}
}
class Penjaga
{
public function beriMakan(MakananKucing $makanan): string
{
return "Penjaga memberi {$makanan->label()}";
}
}
class PenjagaSerbaguna extends Penjaga
{
public function beriMakan(MakananHewan $makanan): string
{
return "Penjaga serbaguna memberi {$makanan->label()}";
}
}
echo (new PenjagaSerbaguna())->beriMakan(new MakananKucing()), "\n";Output yang Dihasilkan:
Penjaga serbaguna memberi makanan kucingPerhatikan tiga hal pada contoh di atas:
Penjaga::beriMakan(MakananKucing $makanan): kontrak parent hanya menerima makanan kucing — kontrak yang sempit.PenjagaSerbaguna::beriMakan(MakananHewan $makanan): override memperluas parameter menjadi interface yang lebih umum. Inilah contravariance.- Pemanggilan
beriMakan(new MakananKucing()): tetap valid karenaMakananKucingmemenuhiMakananHewan; sekaligus pemanggilPenjagaSerbagunakini juga boleh memberi makanan hewan jenis lain.
Pelebaran ini aman karena argumen mengalir masuk: semua nilai yang sah untuk parent tetap diterima anak. Kode lama yang memanggil beriMakan() melalui Penjaga tidak akan pernah mendapati penolakan TypeError dari PenjagaSerbaguna.
Riwayat Dukungan di PHP #
Dukungan variance PHP tumbuh bertahap. Pada PHP 5 hingga 7.1, signature method anak harus identik dengan parent — parameter dan return tidak boleh berubah sama sekali. PHP 7.2 melonggarkan aturan pertama: parameter boleh dilepas tipenya seluruhnya (parameter type widening), sehingga method anak bisa menerima nilai apa pun. Baru pada PHP 7.4 kedua arah lengkap: covariance untuk return type dan contravariance penuh untuk parameter type mendarat bersamaan, seperti tercatat pada panduan migrasi PHP 7.4 .
Artinya, kode modern boleh menulis override yang lebih ekspresif tanpa memecah kompatibilitas. Aturan lengkapnya tetap dirangkum pada halaman variance di manual OOP , dan desain alurnya dapat ditelusuri lewat RFC resmi .
Keduanya Bersama Menjaga Liskov #
Covariance dan contravariance mencapai kekuatan penuhnya saat bekerja dalam satu class yang sama. Class KlinikSpesialis berikut mengoverride dua method sekaligus — satu covariant, satu contravariant:
<?php
abstract class Hewan
{
abstract public function suara(): string;
}
class Kucing extends Hewan
{
public function suara(): string
{
return "meong";
}
}
class Makanan
{
}
class MakananKucing extends Makanan
{
}
class KlinikHewan
{
public function pulangkan(): Hewan
{
throw new LogicException("Belum diimplementasikan");
}
public function rawat(MakananKucing $makanan): void
{
echo "Klinik memberi makanan kucing\n";
}
}
class KlinikSpesialis extends KlinikHewan
{
public function pulangkan(): Kucing
{
return new Kucing();
}
public function rawat(Makanan $makanan): void
{
echo "Klinik memberi makanan apa pun\n";
}
}
$klinik = new KlinikSpesialis();
echo $klinik->pulangkan()->suara(), "\n";
$klinik->rawat(new MakananKucing());Output yang Dihasilkan:
meong
Klinik memberi makanan apa punMari kita urai kombinasi di atas:
pulangkan(): Kucing(covariance): klinik spesialis menjanjikan hasil lebih spesifik daripadaHewanyang parent janjikan.rawat(Makanan $makanan)(contravariance): klinik spesialis menerima jangkauan makanan lebih luas daripadaMakananKucingyang parent terima.$klinik->pulangkan()->suara(): pemanggil menikmati tipe sempitKucingsecara langsung.- Substitusi penuh: object
KlinikSpesialisdapat menggantikanKlinikHewandi mana pun — semua janji parent tetap terpenuhi. Inilah wujud teknis Liskov Substitution Principle yang dibedah pada Chapter Liskov Substitution .
Bila hanya salah satu arah yang tersedia — misalnya PHP lama yang hanya mengizinkan invariance — kombinasi seperti ini mustahil ditulis tanpa melemahkan tipe di tempat lain.
Pelanggaran Adalah Fatal Error #
Dua arah terlarang langsung dihentikan PHP saat class dideklarasikan. Pertama, memperluas return type melanggar janji parent:
<?php
abstract class Hewan {}
class Kucing extends Hewan {}
class KandangKucing
{
public function penghuni(): Kucing
{
return new Kucing();
}
}
class KandangHewan extends KandangKucing
{
public function penghuni(): Hewan
{
return new Kucing();
}
}Output yang Dihasilkan:
Fatal error: Declaration of KandangHewan::penghuni(): Hewan must be compatible with KandangKucing::penghuni(): Kucing in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/covariance-dan-contravariance/.verify_tmp.php on line 16Pesan Declaration of KandangHewan::penghuni(): Hewan must be compatible with KandangKucing::penghuni(): Kucing menegaskan bahwa janji tidak boleh dilonggarkan turun-temurun. Kedua, menyempitkan parameter menutup pintu bagi pemanggil parent:
<?php
class Makanan {}
class MakananKucing extends Makanan {}
class TempatMakanHewan
{
public function isi(Makanan $makanan): void
{
}
}
class TempatMakanKucing extends TempatMakanHewan
{
public function isi(MakananKucing $makanan): void
{
}
}Output yang Dihasilkan:
Fatal error: Declaration of TempatMakanKucing::isi(MakananKucing $makanan): void must be compatible with TempatMakanHewan::isi(Makanan $makanan): void in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/covariance-dan-contravariance/.verify_tmp.php on line 15Kedua fatal error di atas muncul sebelum satu baris logika pun berjalan — pemeriksaan terjadi ketika engine memproses deklarasi class. Kode yang melanggar variance tidak akan pernah lolos ke produksi secara diam-diam.
Parameter By-Reference dan Jebakannya #
Parameter by-reference (&$nilai) mengalir dua arah: nilai masuk ke method lalu kembali ke variabel pemanggil. Override dengan tipe identik bekerja normal:
<?php
class Penghitung
{
public function naikkan(int &$nilai): void
{
$nilai += 1;
}
}
class PenghitungBonus extends Penghitung
{
public function naikkan(int &$nilai): void
{
$nilai += 10;
}
}
$skor = 5;
(new PenghitungBonus())->naikkan($skor);
echo $skor, "\n";Output yang Dihasilkan:
15Mengganti tipe dengan jenis yang tidak berhubungan langsung fatal, sama seperti parameter biasa:
<?php
class Penghitung
{
public function naikkan(int &$nilai): void
{
$nilai += 1;
}
}
class PenghitungSalah extends Penghitung
{
public function naikkan(string &$nilai): void
{
$nilai = "naik";
}
}Output yang Dihasilkan:
Fatal error: Declaration of PenghitungSalah::naikkan(string &$nilai): void must be compatible with Penghitung::naikkan(int &$nilai): void in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/covariance-dan-contravariance/.verify_tmp.php on line 13Namun ada satu perilaku yang mengejutkan: mengganti int &$nilai dengan pelebaran union seperti int|string &$nilai justru diterima, karena deklarasi by-reference mengikuti contravariance — anak wajib menerima setidaknya semua yang parent terima. Pemeriksaan tipe by-reference bersifat sesaat (moment-in-time): dilakukan saat pemanggilan dan saat penulisan ke parameter, bukan selama masa hidup referensi. Akibatnya, tulis balik bisa membocorkan nilai di luar janji parent:
<?php
class Laporan
{
public function tulis(int &$nilai): void
{
$nilai = 99;
}
}
class LaporanLonggar extends Laporan
{
public function tulis(int|string &$nilai): void
{
$nilai = "sembilan puluh sembilan";
}
}
function jalankan(Laporan $laporan): void
{
$skor = 5;
$laporan->tulis($skor);
var_dump($skor);
}
jalankan(new LaporanLonggar());Output yang Dihasilkan:
string(23) "sembilan puluh sembilan"Fungsi jalankan() menerima parameter bertipe Laporan yang menjanjikan int &$nilai, tetapi object yang benar-benar berjalan adalah LaporanLonggar bertipe int|string &$nilai — sehingga penulisan string lolos dan variabel $skor berubah menjadi string(23). Program berakhir tanpa error sama sekali.
Ringkasan #
Covariance menyempitkan return, contravariance memperluas parameter, dan keduanya digabung dalam satu class akan menghasilkan hierarchy yang dapat disubstitusi penuh — inti dari Liskov Substitution Principle. PHP menegakkan seluruh aturan ini dengan fatal error sejak deklarasi, dengan satu nuansa: parameter by-reference yang diubah tipenya hanya ditolak bila menyempit. Dengan variance tuntas, satu wilayah tipe tinggal yang belum tersentuh runtime PHP: koleksi dan class yang parametris oleh tipe lain — wilayah generics yang kita bangun lewat anotasi PHPDoc pada Chapter berikutnya.