Framework sering perlu tahu hal-hal yang tidak bisa diekspresikan lewat kode biasa: method ini menangani rute /berita, class ini adalah table pengguna, atau test ini butuh data provider tertentu. Sebelum PHP 8.0, informasi semacam ini ditulis sebagai komentar docblock dan dibaca ulang dengan parser teks — lambat, tidak ditata lewat sistem tipe, dan salah ketik tidak terdeteksi. PHP 8.0 membawa solusi native bernama attribute: metadata terstruktur yang dideklarasikan dengan sintaks #[...] dan bisa dibaca secara programatik lewat reflection. Pada chapter ini kita pelajari cara mendeklarasikan attribute class, menempelkannya, lalu membacanya untuk membangun perilaku nyata seperti router mini.

Dari Docblock ke Attribute #

Mari kita lihat gaya lama yang selama bertahun-tahun menjadi standar de facto:

text
/**
 * @Route("/berita", methods={"GET"})
 * @Cache(ttl=3600)
 */
public function indeks() {}

Docblock di atas hanya komentar biasa bagi PHP — nilai @Route dan @Cache tinggal teks bebas di dalam string besar. Framework harus menulis parser sendiri untuk mengekstraknya, dan kesalahan kecil seperti kutip yang tidak tertutup tidak pernah diketahui PHP sendiri. Attribute memindahkan metadata ini ke dalam bahasa:

php
<?php

class BeritaController
{
    #[Route("/berita")]
    #[Cache(3600)]
    public function indeks() {}
}

Potongan ini hanyalah ilustrasi bentuk — #[Route("/berita")] dan #[Cache(3600)] adalah metadata yang ditata, bukan komentar. Nama di dalam #[...] merujuk pada class nyata, argumennya dievaluasi mengikuti aturan PHP, dan keseluruhan informasi bisa diperiksa lewat reflection. Dua syarat menjadikannya bekerja: class attribute yang sah, dan kode yang membacanya.

Mendeklarasikan Attribute Class #

Attribute tidak dibuat dari udara; kita mendeklarasikannya sebagai class biasa bertanda #[Attribute] dari class bawaan Attribute. Class inilah yang menentukan argumen apa saja yang boleh diterima:

php
<?php

#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
    public function __construct(public string $path) {}
}

Konstruktor class attribute bertindak sebagai kontrak argumen: #[Route("/tentang")] memanggil konstruktor tersebut dengan "/tentang", dan property $path yang dipromosikan menyimpan nilainya. Argumen pertama #[Attribute(...)] membatasi di mana attribute boleh menempel — memakai konstanta seperti Attribute::TARGET_METHOD atau Attribute::TARGET_CLASS:

Konstanta targetBisa menempel pada
Attribute::TARGET_CLASSdeklarasi class, interface, trait, enum
Attribute::TARGET_METHODmethod
Attribute::TARGET_PROPERTYproperty
Attribute::TARGET_PARAMETERparameter function/method
Attribute::TARGET_ALLsemua tempat di atas

Membaca Attribute dengan Reflection #

Attribute tidak melakukan apa pun saat kode berjalan — ia harus dibaca. Alat bacanya adalah reflection, dan hasil pembacaannya berupa object attribute yang siap dipakai:

php
<?php

#[Attribute]
class Route
{
    public function __construct(public string $path) {}
}

class HalamanController
{
    #[Route("/tentang")]
    public function tentang(): string
    {
        return "Halaman Tentang";
    }
}

$refleksi = new ReflectionClass(HalamanController::class);
$method = $refleksi->getMethod("tentang");

$atribut = $method->getAttributes(Route::class)[0];
$route = $atribut->newInstance();

echo "Rute terdaftar: {$route->path}\n";

Output yang Dihasilkan:

text
Rute terdaftar: /tentang

Mari kita bedah langkah-langkahnya:

  1. Baris 12–13 (#[Route("/tentang")]): attribute Route ditempelkan tepat sebelum method tentang(). PHP menyimpan metadata ini bersama struktur method, tanpa menjalankan constructor Route pada tahap ini.
  2. Baris 20 (new ReflectionClass(...)): kita membuat refleksi dari class HalamanController, lalu mengambil refleksi method tentang lewat getMethod("tentang").
  3. Baris 23 (getAttributes(Route::class)): method ini mengembalikan array ReflectionAttribute yang menempel pada method tersebut. Memberi argumen nama class memfilter hasilnya agar hanya attribute Route yang diambil — indeks [0] mengambil yang pertama.
  4. Baris 24 (newInstance()): baris inilah yang benar-benar menjalankan constructor Route dengan argumen yang tersimpan ("/tentang"), menghasilkan object Route biasa dengan property $path terisi.

Argumen Posisional dan Bernama #

Karena constructor attribute adalah constructor biasa, semua gaya argumen PHP berlaku — termasuk named arguments dan nilai default:

php
<?php

#[Attribute(Attribute::TARGET_CLASS)]
class Deskripsi
{
    public function __construct(
        public string $nama,
        public string $versi = "1.0",
    ) {}
}

#[Deskripsi(nama: "Pustaka Pembayaran", versi: "2.3")]
class PustakaPembayaran {}

$atribut = (new ReflectionClass(PustakaPembayaran::class))
    ->getAttributes(Deskripsi::class)[0];

$meta = $atribut->newInstance();
echo "{$meta->nama} versi {$meta->versi}\n";

Output yang Dihasilkan:

text
Pustaka Pembayaran versi 2.3

Argumen nama: "Pustaka Pembayaran" dipetakan ke parameter $nama secara eksplisit, sedangkan versi: "2.3" menimpa default "1.0". Pattern ini populer untuk metadata bersifat opsional: sebagian besar penempatan cukup menulis argumen wajib, dan yang butuh penyesuaian menambahkan named argument selebihnya. Validasi pun terjadi secara alami — constructor boleh berisi logika pengecekan, dan tipe parameter dijaga PHP.

Attribute Berulang dengan IS_REPEATABLE #

Satu method kadang perlu membawa beberapa metadata sejenis: beberapa middleware, beberapa listener. Secara bawaan, attribute yang sama hanya boleh menempel sekali; untuk mengizinkan pengulangan, tambahkan flag Attribute::IS_REPEATABLE:

php
<?php

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Middleware
{
    public function __construct(public string $nama) {}
}

class ApiController
{
    #[Middleware("auth")]
    #[Middleware("throttle:60")]
    public function dataPengguna(): string
    {
        return "Data pengguna rahasia";
    }
}

$method = new ReflectionMethod(ApiController::class, "dataPengguna");

foreach ($method->getAttributes(Middleware::class) as $atribut) {
    echo "Middleware aktif: " . $atribut->newInstance()->nama . "\n";
}

Output yang Dihasilkan:

text
Middleware aktif: auth
Middleware aktif: throttle:60

Deklarasi Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE menggabungkan dua flag dengan operator bitwise | — target tetap method, dan pengulangan diizinkan. Dua attribute Middleware yang menempel pada dataPengguna() lalu dikembalikan getAttributes() sebagai array dua elemen, terurut sesuai urutan penulisan. Tanpa flag tersebut, PHP menolak penulisan kedua dengan pesan bahwa attribute tidak boleh diulang.

Contoh Dunia Nyata: Router Mini #

Mari kita rakit semua yang sudah dipelajari menjadi alat yang benar-benar bekerja: router yang memindai controller dan mendaftarkan setiap method ber-atribut Route ke tabel rute:

php
<?php

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Route
{
    public function __construct(public string $path) {}
}

class BeritaController
{
    #[Route("/berita")]
    public function indeks(): string
    {
        return "Daftar berita";
    }

    #[Route("/berita/terbaru")]
    public function terbaru(): string
    {
        return "Berita terbaru";
    }
}

$routes = [];

foreach (get_class_methods(BeritaController::class) as $namaMethod) {
    $method = new ReflectionMethod(BeritaController::class, $namaMethod);
    foreach ($method->getAttributes(Route::class) as $atribut) {
        $routes[$atribut->newInstance()->path] = $method;
    }
}

foreach ($routes as $path => $method) {
    echo "$path => " . $method->invoke(new BeritaController()) . "\n";
}

Output yang Dihasilkan:

text
/berita => Daftar berita
/berita/terbaru => Berita terbaru

Alur kerjanya tiga tahap. Pertama, get_class_methods() mengumpulkan seluruh nama method publik milik BeritaController. Kedua, untuk tiap method kita membuka refleksinya dan mencari attribute Route; setiap attribute yang ditemukan dikonversi menjadi object, lalu path-nya menjadi key tabel $routes dengan ReflectionMethod sebagai nilainya. Ketiga, tabel dipakai untuk mencocokkan request: $method->invoke(...) menjalankan handler terkait. Inilah pola dasar yang dipakai framework seperti Symfony dan Laravel — dengan tambahan HTTP method, middleware, dan komponen request yang lebih lengkap.

Attribute Tidak Menjalankan Apa pun Sendirian #

Kesalahpahaman paling umum: menempelkan attribute dan berharap perilakunya muncul dengan sendirinya. Attribute murni metadata — berikut class bertanda Cachable yang tidak membuat class-nya otomatis ter-cache:

php
<?php

#[Attribute]
class Cachable
{
    public function __construct(public int $ttl) {}
}

#[Cachable(3600)]
class LaporanPenjualan
{
    // Tidak ada kode caching apa pun di sini.
    // Attribute hanya menempel sebagai metadata;
    // kode lain harus membacanya dan bertindak.
}

Menjalankan program di atas tidak memicu apa pun — tidak ada cache yang terbentuk sampai sebuah cache layer membaca Cachable lewat reflection dan memutuskan tindakannya. Di ekosistem nyata, pihak yang membaca adalah framework: PHPUnit membaca #[Test] dan #[DataProvider] untuk menjalankan pengujian, Doctrine ORM membaca #[ORM\Table] untuk memetakan class ke tabel database, dan Symfony membaca #[Route] untuk mendaftarkan rute. Berikut gaya PHPUnit modern yang menggantikan anotasi docblock lama:

php
<?php

use PHPUnit\Framework\Attributes\Test;

final class KalkulatorTest
{
    #[Test]
    public function penjumlahan(): void
    {
        // dijalankan oleh phpunit, bukan php biasa
    }
}

Ringkasan #

Attribute memberi PHP jalur metadata yang ditata: class attribute mendefinisikan kontrak argumen lewat constructor, sintaks #[...] menempelkannya pada class/method/property, dan reflection membacanya kapan pun diperlukan. Kita sudah melihat keseluruhan siklusnya — dari deklarasi, penempatan, pembacaan, sampai router mini yang mengubah metadata menjadi perilaku. Satu aktor utama belum kita bedah secara khusus: reflection sendiri. Chapter berikutnya menyusuri ReflectionClass, ReflectionMethod, dan kawan-kawannya sebagai alat inspeksi dan manipulasi struktur kode.

Referensi #