Type declaration native yang kita bangun sepanjang section ini memiliki batas kosakata yang jelas. Sintaks array tidak bisa menyatakan apa isi array tersebut, callable tidak bisa menyatakan parameter dan nilai kembali yang diharapkan, dan tidak ada cara native untuk mendeskripsikan struktur array asosiatif. PHPDoc menutup celah tersebut lewat anotasi berformat standar yang ditulis di dalam docblock. Pada Chapter ini kita akan mempelajari anotasi inti @param, @return, @var, @property, dan @method, lalu menyusun tipe kaya seperti array<int, string>, class-string, dan array shape yang dipahami oleh IDE maupun static analyzer.

Mengapa Masih Perlu PHPDoc #

PHPDoc adalah konvensi anotasi standar industri yang ditulis sebagai blok komentar /** ... */ tepat sebelum class, function, method, atau property. Runtime PHP sama sekali tidak mengeksekusi isi docblock — docblock adalah komentar biasa bagi interpreter. Nilainya justru ada di alat lain yang membacanya: IDE memakainya untuk autocompletion dan peringatan, sedangkan PHPStan dan Psalm memakainya sebagai sumber kebenaran tipe ketika type declaration native tidak cukup ekspresif.

Perhatikan tiga kelemahan kosakata native berikut:

  1. array hanya menyatakan “ini array”, tanpa kunci dan tipe isinya.
  2. callable hanya menyatakan “ini bisa dipanggil”, tanpa signature parameter dan return.
  3. string untuk nama class hanya string biasa, padahal maknanya jauh lebih spesifik.

Anotasi PHPDoc berikut menjadi kosakata pengganti ketiga celah tersebut:

AnotasiTargetFungsi
@param T $nama deskripsifunction/methodtipe sebuah parameter
@return T deskripsifunction/methodtipe nilai kembali
@var T $nama deskripsiproperty/variabeltipe property atau variabel lokal
@property T $namaclassproperty magic hasil __get/__set
@property-read T $namaclassproperty magic yang hanya bisa dibaca
@method T nama(T $arg)classmethod magic hasil __call
@template Tclass/methodparameter tipe generik

Anotasi @template beserta generics sudah kita bahas tuntas pada Chapter Generics dengan PHPDoc , jadi Chapter ini berfokus pada lima anotasi lainnya dan sintaks tipenya.

Anotasi Inti @param, @return, dan @var #

Anotasi @param dan @return melengkapi signature function dengan informasi yang tidak muat di native type. Mari kita lihat contoh function yang menerima dua array asosiatif:

php
<?php

/**
 * Menghitung total harga belanja.
 *
 * @param array<string, int> $keranjang pasangan nama barang => jumlah
 * @param array<string, int> $hargaSatuan pasangan nama barang => harga satuan
 * @return int total harga dalam rupiah
 */
function hitungTotal(array $keranjang, array $hargaSatuan): int
{
    $total = 0;
    foreach ($keranjang as $nama => $jumlah) {
        $total += $hargaSatuan[$nama] * $jumlah;
    }

    return $total;
}

$total = hitungTotal(
    ['Buku Tulis' => 3, 'Pensil' => 5],
    ['Buku Tulis' => 4500, 'Pensil' => 2000]
);

echo "Total belanja: Rp$total\n";

Output yang Dihasilkan:

text
Total belanja: Rp23500

Mari kita baca anotasi pada docblock di atas:

  1. @param array<string, int> $keranjang: parameter pertama adalah array dengan kunci string (nama barang) dan nilai int (jumlah).
  2. @param array<string, int> $hargaSatuan: parameter kedua memiliki bentuk yang sama, tetapi nilainya berarti harga satuan.
  3. @return int: nilai kembali adalah integer rupiah — redundan dengan native : int, tetapi tetap berguna sebagai tempat deskripsi.

Berbekal dua anotasi itu, PHPStan tahu bahwa $hargaSatuan[$nama] * $jumlah adalah int * int, dan IDE dapat melengkapi nama barang saat kita mengetik. Signature native array tetap menjadi penjaga runtime, sedangkan docblock menjadi peta isi bagi analyzer.

Anotasi @var memiliki dua peran. Untuk property, @var menyatakan tipe property — terutama untuk property mixed yang tipe aslinya lebih spesifik. Untuk variabel lokal, @var membantu saat tipe hasil ekspresi tidak bisa disimpulkan:

php
<?php

/** @var array<int, string> $daftarTugas */
$daftarTugas = ['Belajar PHPDoc', 'Menjalankan PHPStan', 'Menulis baseline'];

echo 'Tugas pertama: ', $daftarTugas[0], "\n";
echo 'Jumlah tugas: ', count($daftarTugas), "\n";

Output yang Dihasilkan:

text
Tugas pertama: Belajar PHPDoc
Jumlah tugas: 3

Anotasi @var pada baris sebelum penugasan memberi tahu analyzer bahwa $daftarTugas adalah daftar string berindeks integer. Pola @var untuk variabel lokal paling sering dipakai tepat setelah preg_match(), json_decode(), atau pengambilan data dari array besar yang bentuknya sudah diketahui penulis kode.

Sintaks Tipe di Luar Kemampuan Native #

Kekuatan sebenarnya PHPDoc ada pada kosakata tipenya. Tabel berikut merangkum bentuk-bentuk yang paling sering dipakai, lengkap dengan padanan nativenya:

Sintaks PHPDocArtiPadanan native
array<int, string>array berkunci int, berisi stringarray
list<string>array berurutan (kunci 0, 1, 2, …) berisi stringarray
array{ok: bool, data: T}array shape: kunci dan tipe per field tetaparray
T|null atau ?Tnilai bertipe T atau null?T
class-stringstring berisi nama class yang validstring
class-string<T>string berisi nama class turunan Tstring
callable(int): stringcallable dengan satu parameter int, mengembalikan stringcallable
iterable<T>array<T> atau Traversable<T>iterable

Tipe class-string layak mendapat contoh karena sering muncul di factory dan container:

php
<?php

/**
 * Membuat instance baru dari nama class.
 *
 * @param class-string $namaClass nama class yang bisa di-instantiasi
 */
function buatInstance(string $namaClass): object
{
    return new $namaClass();
}

$tanggal = buatInstance(DateTimeImmutable::class);

echo get_class($tanggal), "\n";

Output yang Dihasilkan:

text
DateTimeImmutable

Bagi runtime, $namaClass hanyalah string — operator new menerima string apa pun yang berupa nama class. Bagi analyzer, class-string jauh lebih ketat: jika kita memanggil buatInstance('Budi'), PHPStan menolaknya karena 'Budi' tidak bisa dibuktikan sebagai nama class. Varian class-string<T> mempersempit lebih lagi ke class tertentu, misalnya class-string<Pesanan> hanya menerima Pesanan::class atau nama class turunannya.

Array Shape untuk Data Terstruktur #

Array asosiatif dengan kunci tetap adalah pola data yang sangat umum: hasil query database, payload API, baris CSV. Native hanya bisa menulis array; PHPDoc dapat menuliskan array shape — daftar field beserta tipenya:

php
<?php

/**
 * Mengambil data produk dari katalog.
 *
 * @return array{nama: string, harga: int, stok: int}
 */
function cariProduk(string $kode): array
{
    return [
        'nama' => 'Buku Tulis Sidu',
        'harga' => 4500,
        'stok' => 12,
    ];
}

$produk = cariProduk('B001');

echo "{$produk['nama']}: Rp{$produk['harga']} (stok {$produk['stok']})\n";

Output yang Dihasilkan:

text
Buku Tulis Sidu: Rp4500 (stok 12)

Shape array{nama: string, harga: int, stok: int} menyatakan tiga hal sekaligus kepada analyzer dan IDE: field apa saja yang ada, tipe tiap field, dan bahwa tidak ada field lain. Akibatnya, $produk['hrga'] (salah ketik) langsung ditandai sebagai field yang tidak ada dalam shape, dan $produk['harga'] + 1 diyakini aman karena bertipe int. Pada runtime, nilai yang dikembalikan tetap array biasa — tidak ada validasi otomatis; shape hanyalah kontrak di atas kertas (yang ditagih oleh analyzer).

@property dan @method untuk Anggota Magic #

Method magic __get() dan __call() membuat property dan method tidak terlihat oleh IDE maupun analyzer, karena keduanya tidak benar-benar dideklarasikan. PHPDoc pada level class dapat menyatakan kontrak anggota magic tersebut:

php
<?php

/**
 * @property-read int $saldo saldo rekening dalam rupiah
 * @method int tarik(int $jumlah) mengurangi saldo dan mengembalikan sisa
 */
class Rekening
{
    public function __get(string $nama): mixed
    {
        return match ($nama) {
            'saldo' => 250000,
            default => null,
        };
    }

    public function __call(string $nama, array $argumen): mixed
    {
        return match ($nama) {
            'tarik' => 250000 - $argumen[0],
            default => null,
        };
    }
}

$rekening = new Rekening();

echo 'Saldo: ', $rekening->saldo, "\n";
echo 'Setelah tarik: ', $rekening->tarik(50000), "\n";

Output yang Dihasilkan:

text
Saldo: 250000
Setelah tarik: 200000

Mari kita bedah docblock class di atas:

  1. @property-read int $saldo: class memiliki property magic hanya-baca bernama $saldo bertipe int — akhiran -read karena __get() hanya menyediakan pembacaan.
  2. @method int tarik(int $jumlah): class memiliki method magic tarik() yang menerima int dan mengembalikan int — kontrak yang dipenuhi implementasi __call() pada baris di bawahnya.

Setelah dua anotasi ini terpasang, IDE melengkapi $rekening->ta... menjadi tarik(), dan analyzer memeriksa bahwa $rekening->tarik('lima puluh') melanggar kontrak int $jumlah. Tanpa anotasi, keduanya buta terhadap anggota magic.

Kontrak Tanpa Penegak di Runtime #

Sifat PHPDoc sebagai komentar memiliki konsekuensi penting: runtime tidak pernah memvalidasi isinya, bahkan ketika isinya jelas-jelas salah. Perhatikan docblock berikut yang berbohong total:

php
<?php

/**
 * @param Gajah $hewan tipe yang tidak ada sekalipun tetap diabaikan
 * @return string[] klaim palsu: sebenarnya mengembalikan string
 */
function suara($hewan)
{
    return 'pret';
}

echo suara('kuda'), "\n";

Output yang Dihasilkan:

text
pret

Program berjalan mulus dan mencetak pret, meskipun tipe Gajah tidak pernah didefinisikan dan nilai kembali bertentangan dengan klaim string[]. Interpreter hanya melihat function suara() tanpa parameter bertipe dan return 'pret' — docblock dilewati begitu saja. Inilah sebabnya PHPDoc tidak pernah menggantikan type declaration native: native dijaga runtime, PHPDoc dijaga alat pihak luar.

Ringkasan #

PHPDoc memperluas kosakata tipe jauh melampaui kemampuan native: @param dan @return memetakan isi array, @var menandai property dan variabel lokal, @property dan @method mendokumentasikan anggota magic, sementara sintaks seperti array<int, string>, list<T>, class-string<T>, dan array shape memberi tipe yang presisi. Semua anotasi ini hidup sebagai kontrak di atas kertas — runtime mengabaikannya sepenuhnya. Kontrak baru bermakna jika ada yang menegakkannya, dan itulah peran static analysis yang akan kita selami pada Chapter berikutnya.