Bayangkan sebuah class koleksi yang dipastikan hanya berisi object Pengguna, atau sebuah Repository<T> yang method cari()-nya dijamin mengembalikan entitas yang tepat tanpa casting. Bahasa seperti Java dan C# memilikinya sebagai generics bawaan — PHP tidak. Kita perlu jujur sejak awal: PHP tidak memiliki generics pada level runtime, dan seluruh tata cara Kotak<int> yang akan kita tulis di Chapter ini hidup di dalam komentar PHPDoc. Yang menyelamatkan pendekatan ini adalah static analysis: PHPStan dan Psalm membaca anotasi tersebut, mengikat parameter tipe, dan menolak kode yang melanggar kontrak — sebelum program berjalan.

PHP Tidak Punya Generics Runtime #

Pada runtime, array dan object PHP tidak menyimpan informasi tipe parametris. Sebuah array berisi int dan array berisi object Pengguna adalah struktur yang persis sama, dan operator instanceof tidak memiliki cara membedakan “koleksi dari X” dengan “koleksi dari Y”. Usulan generics untuk PHP pernah ditulis sebagai RFC resmi , namun tidak pernah diadopsi karena kompleksitas implementasi dan biaya performa yang menyertainya.

Ekosistem PHP menjawab kebutuhan ini dengan konvensi: tanda tangan tipe generik ditulis sebagai anotasi PHPDoc, lalu diperiksa oleh static analyzer. Semua contoh pada Chapter ini adalah PHP biasa yang berjalan mulus tanpa analyzer — namun nilai sebenarnya baru muncul ketika PHPStan atau Psalm ikut berjalan di pipeline Anda. Jika belum memasang keduanya, panduan pemasangannya tersedia di dokumentasi PHPStan dan dokumentasi Psalm .

Anotasi @template #

Batu bata dasar generics PHPDoc adalah @template, yang mendeklarasikan parameter tipe — placeholder bernama yang akan diikat ketika function dipanggil:

php
<?php

/**
 * Mengembalikan nilai apa adanya tanpa perubahan.
 *
 * @template T
 * @param T $nilai
 * @return T
 */
function identitas(mixed $nilai): mixed
{
    return $nilai;
}

$nama = identitas("Budi");
$versi = identitas(2);

echo $nama, "\n";
echo $versi, "\n";

Output yang Dihasilkan:

text
Budi
2

Mari kita baca anotasi pada blok docblock di atas:

  1. @template T: mendeklarasikan parameter tipe bernama T milik function identitas().
  2. @param T $nilai: tipe parameter sesungguhnya adalah T, bukan mixed — mixed pada signature runtime hanya wadah karena PHP tidak punya sintaks untuk T.
  3. @return T: nilai kembali bertipe sama dengan masukan.
  4. Pengikatan saat pemanggilan: identitas("Budi") mengikat T ke string, sehingga PHPStan menyimpulkan $nama bertipe string; identitas(2) mengikat T ke int sehingga $versi bertipe int.

Runtime sama sekali tidak melihat anotasi ini — program mencetak Budi dan 2 seperti function mixed biasa. Seluruh daya analisanya ada di sisi alat.

Class Generik Kotak<T> #

Parameter type @template juga dapat dimiliki oleh class, sehingga setiap instance membawa pengikatan tipe tersendiri. Pola paling sederhananya adalah wadah nilai tunggal:

php
<?php

/**
 * Wadah sederhana yang menyimpan satu nilai.
 *
 * @template TItem
 */
class Kotak
{
    /** @param TItem $isi */
    public function __construct(private mixed $isi)
    {
    }

    /** @return TItem */
    public function ambil(): mixed
    {
        return $this->isi;
    }
}

/**
 * @param Kotak<int> $kotak
 */
function tampilkanAngka(Kotak $kotak): void
{
    echo $kotak->ambil() + 1, "\n";
}

tampilkanAngka(new Kotak(41));

Output yang Dihasilkan:

text
42

Tiga lapis anotasi bekerja sama pada contoh di atas. Pertama, @template TItem pada class Kotak menjadikan TItem parameter tipe yang dikenali di seluruh docblock class. Kedua, @param TItem $isi pada constructor membuat new Kotak(41) diikat sebagai Kotak<int> dan new Kotak(" halo ") sebagai Kotak<string> — pengikatan ditentukan oleh tipe argumen yang diberikan. Ketiga, anotasi @param Kotak<int> $kotak pada function tampilkanAngka() menyatakan bahwa function tersebut hanya menerima kotak berisi int; di dalam body, PHPStan tahu bahwa $kotak->ambil() mengembalikan int, sehingga operasi + 1 dianggap aman.

Pada runtime, Kotak hanyalah class biasa dan program mencetak 42 tanpa sepengetahuan tentang TItem. Tidak ada sintaks new Kotak<int>() — pengikatan selalu lewat anotasi dan inferensi.

Pola Repository<T> #

Pola paling populer untuk generics PHPDoc adalah Repository<T>. Satu interface generik mendeskripsikan operasi penyimpanan, lalu setiap entitas mengikat parameter tipe-nya sendiri:

php
<?php

class Pengguna
{
    public function __construct(public string $nama)
    {
    }
}

/**
 * @template TEntity
 */
interface Repositori
{
    /** @return TEntity|null */
    public function cari(int $id): mixed;

    /** @param TEntity $entitas */
    public function simpan(mixed $entitas): void;
}

/**
 * @extends Repositori<Pengguna>
 */
class RepositoriPengguna implements Repositori
{
    /** @var array<int, Pengguna> */
    private array $data = [];

    public function __construct()
    {
        $this->data[1] = new Pengguna("Budi");
    }

    public function cari(int $id): mixed
    {
        return $this->data[$id] ?? null;
    }

    public function simpan(mixed $entitas): void
    {
        echo "Menyimpan {$entitas->nama}\n";
    }
}

$repositori = new RepositoriPengguna();
echo $repositori->cari(1)?->nama, "\n";
echo $repositori->cari(99)?->nama ?? "tidak ditemukan", "\n";
$repositori->simpan(new Pengguna("Siti"));

Output yang Dihasilkan:

text
Budi
tidak ditemukan
Menyimpan Siti

Mari kita urai peran tiap anotasi:

  1. @template TEntity pada interface: setiap method boleh memakai TEntity sebagai tipe — cari() mengembalikannya (atau null), simpan() menerimanya.
  2. @extends Repositori<Pengguna>: mengikat TEntity = Pengguna untuk RepositoriPengguna. Sejak baris ini, PHPStan tahu $repositori->cari(1) mengembalikan Pengguna|null, sehingga nullsafe ?->nama diperbolehkan dan cari(99)?->nama ?? "..." dianalisis tepat.
  3. @var array<int, Pengguna>: memberi tahu analyzer bahwa penyimpanan internal memang berisi Pengguna yang dipetakan per int.
  4. Konsekuensi penegakan: memanggil $repositori->simpan(new Pesanan(...)) akan ditolak analyzer — Pesanan bukan Pengguna, padahal signature runtime hanya melihat mixed.

Pola ini menghilangkan duplikasi: satu interface Repositori melayani RepositoriPengguna, RepositoriPesanan, dan seterusnya, masing-masing dengan tipe yang terdokumentasi penuh.

Template Covariant #

Sama seperti return type pada Chapter Variance , parameter tipe pun punya arah. @template-covariant T menyatakan bahwa T hanya keluar dari class (posisi return), sehingga Penampung<Kucing> boleh dipakai di tempat Penampung<Hewan> diharapkan:

php
<?php

/**
 * @template-covariant T
 */
interface Penampung
{
    /** @return T */
    public function ambil(): mixed;
}

class Hewan
{
    public function suara(): string
    {
        return "suara hewan";
    }
}

class Kucing extends Hewan
{
    public function suara(): string
    {
        return "meong";
    }
}

/** @implements Penampung<Kucing> */
class KandangKucing implements Penampung
{
    public function ambil(): mixed
    {
        return new Kucing();
    }
}

/**
 * @param Penampung<Hewan> $penampung
 */
function dengarkan(Penampung $penampung): void
{
    echo $penampung->ambil()->suara(), "\n";
}

dengarkan(new KandangKucing());

Output yang Dihasilkan:

text
meong

Fungsi dengarkan() dianotasi menerima Penampung<Hewan>, dan kita mengirim KandangKucing yang merupakan Penampung<Kucing> — diterima analyzer karena T covariant. Bila @template biasa (invariant) dipakai, kombinasi tersebut akan dilaporkan sebagai pelanggaran tipe. Aturan praktisnya: gunakan @template-covariant untuk wadah yang hanya menghasilkan nilai T (producer), @template biasa untuk wadah yang menerima sekaligus mengembalikan, dan @template-contravariant — didukung Psalm — untuk yang hanya mengonsumsi T. Daftar lengkap anotasinya terdokumentasi pada referensi templated annotations Psalm .

Batas Runtime — Tipe Generik Terhapus #

Karena generics hidup di PHPDoc, seluruhnya terhapus saat eksekusi. Kotak<int> dan Kotak<string> adalah class Kotak yang sama: new Kotak(41) instanceof Kotak dan new Kotak("hi") instanceof Kotak sama-sama true, dan tidak ada mekanisme runtime yang bisa membedakannya. Penegakan hanya terjadi pada alat analisis. Bila kita sengaja mengirim kotak berisi string ke tampilkanAngka(), PHPStan menolaknya dengan pesan yang kurang lebih berbunyi:

text
Parameter #1 $kotak of function tampilkanAngka() expects Kotak<int>,
Kotak<string> given.

Pesan tersebut muncul tanpa menjalankan satu baris pun dari program — analisis dilakukan terhadap kode sumber. Sebaliknya, bila analyzer dilewati, PHP akan menjalankan kode itu tanpa protes awal dan hanya gagal nanti bila operasi aritmetikanya menyentuh nilai yang tidak cocok. Maka dari itu, jalankan analyzer sebagai bagian dari CI sama pentingnya dengan menjalankan test.

Alternatif — Class Konkret per Tipe #

Ketika jaminan runtime benar-benar dibutuhkan — misalnya pada batas API yang menerima masukan dari kode tak terpercaya — alternatif yang jujur adalah class konkret dengan signature asli. PHP menegakkan signature asli dengan TypeError nyata:

php
<?php

class Pengguna
{
    public function __construct(public string $nama)
    {
    }
}

final class DaftarPengguna
{
    /** @var list<Pengguna> */
    private array $item = [];

    public function tambah(Pengguna $pengguna): void
    {
        $this->item[] = $pengguna;
    }

    public function namaSemua(): string
    {
        return implode(", ", array_map(fn (Pengguna $p) => $p->nama, $this->item));
    }
}

$daftar = new DaftarPengguna();
$daftar->tambah(new Pengguna("Budi"));
$daftar->tambah(new Pengguna("Siti"));
echo $daftar->namaSemua(), "\n";

Output yang Dihasilkan:

text
Budi, Siti

Method tambah() pada class DaftarPengguna memakai parameter bertipe Pengguna yang sesungguhnya, bukan mixed ber-anotasi. Menyelundupkan object lain akan memicu TypeError fatal di runtime — tanpa analyzer, tanpa CI. Harganya adalah duplikasi: Anda perlu class tersendiri untuk DaftarPesanan, DaftarProduk, dan seterusnya. Proyek nyata umumnya menggabungkan keduanya — generics PHPDoc untuk lapisan domain yang penuh analyzer, dan class konkret pada titik-titik batas yang menuntut jaminan runtime.

Ringkasan #

Generics PHP tidak berjalan di runtime — ia adalah konvensi anotasi yang dihidupkan oleh PHPStan dan Psalm. Dengan @template, @param Kotak<int>, @extends Repositori<Pengguna>, dan @template-covariant, kita mendapatkan koleksi dan repository yang aman tipe tanpa menduplikasi class; dengan class konkret bertipe asli, kita mendapatkan penegakan runtime pada titik kritis. Kosakata anotasi PHPDoc ternyata jauh lebih kaya dari sekadar generics — array shape, tipe literal, hingga conditional type menanti pada Chapter PHPDoc Types .