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:
/**
* @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
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
#[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 target | Bisa menempel pada |
|---|---|
Attribute::TARGET_CLASS | deklarasi class, interface, trait, enum |
Attribute::TARGET_METHOD | method |
Attribute::TARGET_PROPERTY | property |
Attribute::TARGET_PARAMETER | parameter function/method |
Attribute::TARGET_ALL | semua 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
#[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:
Rute terdaftar: /tentangMari kita bedah langkah-langkahnya:
- Baris 12–13 (
#[Route("/tentang")]): attributeRouteditempelkan tepat sebelum methodtentang(). PHP menyimpan metadata ini bersama struktur method, tanpa menjalankan constructorRoutepada tahap ini. - Baris 20 (
new ReflectionClass(...)): kita membuat refleksi dari classHalamanController, lalu mengambil refleksi methodtentanglewatgetMethod("tentang"). - Baris 23 (
getAttributes(Route::class)): method ini mengembalikan arrayReflectionAttributeyang menempel pada method tersebut. Memberi argumen nama class memfilter hasilnya agar hanya attributeRouteyang diambil — indeks[0]mengambil yang pertama. - Baris 24 (
newInstance()): baris inilah yang benar-benar menjalankan constructorRoutedengan argumen yang tersimpan ("/tentang"), menghasilkan objectRoutebiasa dengan property$pathterisi.
Argumen Posisional dan Bernama #
Karena constructor attribute adalah constructor biasa, semua gaya argumen PHP berlaku — termasuk named arguments dan nilai default:
<?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:
Pustaka Pembayaran versi 2.3Argumen 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
#[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:
Middleware aktif: auth
Middleware aktif: throttle:60Deklarasi 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
#[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:
/berita => Daftar berita
/berita/terbaru => Berita terbaruAlur 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
#[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
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.