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
<?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:

text
meong

Mari kita bedah hierarchy di atas:

  1. abstract class Hewan: akar hierarchy dengan method abstrak suara(), sehingga setiap anak wajib mendeklarasikan suaranya sendiri.
  2. Penampungan::adopsi(): Hewan: parent menjanjikan hasil adopsi bertipe paling umum — Hewan — dan implementasi bawaannya mengembalikan Anjing.
  3. PenampunganKucing::adopsi(): Kucing: override dengan return type Kucing, yang lebih spesifik daripada janji parent. Inilah covariance.
  4. $hewan->suara(): pemanggil yang tahu sedang berurusan dengan PenampunganKucing langsung menerima Kucing — termasuk seluruh method khusus kucing bila ada — tanpa pengecekan instanceof tambahan.

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
<?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:

text
Penjaga serbaguna memberi makanan kucing

Perhatikan tiga hal pada contoh di atas:

  1. Penjaga::beriMakan(MakananKucing $makanan): kontrak parent hanya menerima makanan kucing — kontrak yang sempit.
  2. PenjagaSerbaguna::beriMakan(MakananHewan $makanan): override memperluas parameter menjadi interface yang lebih umum. Inilah contravariance.
  3. Pemanggilan beriMakan(new MakananKucing()): tetap valid karena MakananKucing memenuhi MakananHewan; sekaligus pemanggil PenjagaSerbaguna kini 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
<?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:

text
meong
Klinik memberi makanan apa pun

Mari kita urai kombinasi di atas:

  1. pulangkan(): Kucing (covariance): klinik spesialis menjanjikan hasil lebih spesifik daripada Hewan yang parent janjikan.
  2. rawat(Makanan $makanan) (contravariance): klinik spesialis menerima jangkauan makanan lebih luas daripada MakananKucing yang parent terima.
  3. $klinik->pulangkan()->suara(): pemanggil menikmati tipe sempit Kucing secara langsung.
  4. Substitusi penuh: object KlinikSpesialis dapat menggantikan KlinikHewan di 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
<?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:

text

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 16

Pesan 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
<?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:

text

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 15

Kedua 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
<?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:

text
15

Mengganti tipe dengan jenis yang tidak berhubungan langsung fatal, sama seperti parameter biasa:

php
<?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:

text

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 13

Namun 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
<?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:

text
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.