Interface pada chapter sebelumnya membagikan kontrak, inheritance membagikan implementasi — tetapi keduanya punya batas. Inheritance hanya mengizinkan satu parent, sedangkan interface memang tidak membawa implementasi sama sekali. Bagaimana jika beberapa class yang tidak berkerabat membutuhkan method yang persis sama, misalnya kemampuan mencatat waktu dibuat atau menghitung pajak? Menyalin-tempel kode ke setiap class membuat satu perubahan harus diulang di banyak tempat, sedangkan memaksa semua class mewarisi satu parent akan merusak hierarki yang tidak logis. Trait adalah jawaban PHP untuk pemakaian ulang horizontal: blok kode siap pakai yang bisa ditempelkan ke class mana pun lewat kata kunci use.
Trait sebagai Pemakaian Ulang Horizontal #
Trait dideklarasikan dengan kata kunci trait dan berisi property serta method seperti class biasa. Perbedaannya, trait tidak bisa diinstansiasi sendiri; trait baru hidup ketika ditempelkan ke class pemakai lewat statement use. Secara semantik, isi trait “disalin” ke dalam class tersebut, sehingga object hasil instansiasi memperoleh semua property dan method dari trait:
<?php
trait PenandaWaktu
{
public string $dibuatPada = "";
public function tandai(string $waktu): void
{
$this->dibuatPada = $waktu;
}
}
class Artikel
{
use PenandaWaktu;
}
class Komentar
{
use PenandaWaktu;
}
$artikel = new Artikel();
$artikel->tandai("2026-01-15 08:00:00");
$komentar = new Komentar();
$komentar->tandai("2026-01-15 09:30:00");
echo "Artikel dibuat: " . $artikel->dibuatPada . "\n";
echo "Komentar dibuat: " . $komentar->dibuatPada . "\n";Output yang Dihasilkan:
Artikel dibuat: 2026-01-15 08:00:00
Komentar dibuat: 2026-01-15 09:30:00Mari kita bedah kode di atas:
trait PenandaWaktu: mendefinisikan property$dibuatPadabeserta methodtandai()— keduanya akan tersedia bagi setiap class yang memakai trait ini.use PenandaWaktu;: statement di dalam body class menempelkan seluruh isi trait ke classArtikeldanKomentar.- Dua class tanpa hubungan keluarga kini berbagi implementasi yang sama tanpa
extendssama sekali — inilah yang disebut pemakaian ulang horizontal.
Perlu diketahui urutan prioritas ketika nama method bentrok: method yang ditulis langsung di class menang atas method dari trait, dan method dari trait menang atas method yang diwarisi dari parent. Aturan ini penting diingat karena PHP tidak menganggap bentrok tersebut sebagai error — class pemakai selalu menjadi penentu akhir.
Menyelesaikan Konflik Antar Trait #
Satu class boleh memakai banyak trait sekaligus. Masalah muncul ketika dua trait membawa method dengan nama yang sama — PHP menolak menebak pemenangnya dan menghentikan eksekusi. Solusinya adalah operator insteadof untuk memilih pemenang, lalu as untuk memberi nama alias pada yang kalah agar tetap bisa dipanggil:
<?php
trait SapaanFormal
{
public function sapa(): string
{
return "Selamat datang";
}
}
trait SapaanSantai
{
public function sapa(): string
{
return "Halo, apa kabar";
}
}
class Aplikasi
{
use SapaanFormal, SapaanSantai {
SapaanFormal::sapa insteadof SapaanSantai;
SapaanSantai::sapa as sapaSantai;
}
}
$app = new Aplikasi();
echo $app->sapa() . "\n";
echo $app->sapaSantai() . "\n";Output yang Dihasilkan:
Selamat datang
Halo, apa kabarBagian di dalam kurung kurawal setelah daftar trait adalah blok penyelesaian konflik. Baris SapaanFormal::sapa insteadof SapaanSantai menyatakan bahwa nama sapa() diputuskan memakai versi SapaanFormal, sementara baris SapaanSantai::sapa as sapaSantai mendaftarkan versi SapaanSantai dengan nama baru sapaSantai(). Kedua perilaku tetap bisa dipakai berdampingan — hanya namanya yang dipisah.
Property dan Method Abstract dalam Trait #
Trait boleh mengakses property milik class pemakainya lewat $this, seperti contoh berikut:
<?php
trait Bernama
{
public function namaLengkap(): string
{
return $this->namaDepan . " " . $this->namaBelakang;
}
}
class Pengguna
{
use Bernama;
public function __construct(
private string $namaDepan,
private string $namaBelakang,
) {
}
}
$budi = new Pengguna("Budi", "Santoso");
echo $budi->namaLengkap() . "\n";Output yang Dihasilkan:
Budi SantosoTrait Bernama mengasumsikan class pemakai menyediakan property $namaDepan dan $namaBelakang. Asumsi implisit seperti ini rapuh: ketika property tersebut tidak ada, error baru muncul saat method dipanggil. Cara yang lebih aman adalah mendeklarasikan method abstract di dalam trait — class pemakai akan dipaksa menyediakannya, sama seperti kontrak interface:
<?php
trait HargaBersih
{
abstract public function hargaDasar(): float;
public function hargaSetelahPajak(): float
{
return round($this->hargaDasar() * 1.11, 2);
}
}
class ProdukFisik
{
use HargaBersih;
public function hargaDasar(): float
{
return 100000.0;
}
}
$produk = new ProdukFisik();
echo "Harga setelah pajak: Rp" . $produk->hargaSetelahPajak() . "\n";Output yang Dihasilkan:
Harga setelah pajak: Rp111000Method abstract dalam trait mendeklarasikan kebutuhan secara eksplisit: class pemakai wajib menyediakan hargaDasar(), jika tidak PHP memunculkan fatal error. Method konkret hargaSetelahPajak() kemudian bebas memanggil method wajib tersebut karena kontraknya sudah jelas. Terkait property, PHP 8 mengizinkan trait dan class mendeklarasikan property dengan nama yang sama selama nilai awalnya kompatibel; jika nilai awalnya berbeda, deklarasi tersebut berakhir fatal error.
Constructor dalam Trait #
Trait juga boleh membawa constructor, dan pola ini bekerja selama class pemakai tidak mendefinisikan constructor sendiri:
<?php
trait KoneksiBasisData
{
public string $sumberData;
public function __construct(string $sumberData)
{
$this->sumberData = $sumberData;
}
}
class RepositoriPengguna
{
use KoneksiBasisData;
public function sumber(): string
{
return $this->sumberData;
}
}
$repo = new RepositoriPengguna("sqlite::memory:");
echo "Terhubung ke: " . $repo->sumber() . "\n";Output yang Dihasilkan:
Terhubung ke: sqlite::memory:Masalahnya muncul ketika class pemakai kelak menambahkan constructor miliknya sendiri. Sesuai aturan prioritas, constructor class menang secara diam-diam atas constructor trait — perhatikan hasil berikut:
<?php
trait KoneksiBasisData
{
public string $sumberData;
public function __construct(string $sumberData)
{
$this->sumberData = $sumberData;
}
}
class RepositoriPengguna
{
use KoneksiBasisData;
public function __construct()
{
echo "Constructor class sendiri berjalan\n";
}
}
$repo = new RepositoriPengguna();
var_dump(isset($repo->sumberData));Output yang Dihasilkan:
Constructor class sendiri berjalan
bool(false)Constructor milik RepositoriPengguna berjalan, constructor trait tidak pernah dipanggil, dan property $sumberData pun tidak pernah terisi — isset() mengembalikan false tanpa satu pun error yang memberi tahu kita. Karena jebakan senyap inilah constructor dalam trait umumnya dihindari.
Trait vs Interface vs Inheritance #
Ketiga mekanisme ini sering membuat bingung karena semuanya “membagikan sesuatu” antar class. Tabel berikut memetakan perannya:
| Aspek | Inheritance (extends) | Interface (implements) | Trait (use) |
|---|---|---|---|
| Hubungan | is-a (merupakan) | bisa-melakukan | menempelkan implementasi |
| Arah pembagian | Vertikal, parent ke child | Kontrak lintas class | Horizontal, lintas class |
| Jumlah per class | Satu parent | Banyak interface | Banyak trait |
| Membawa kode konkret | Ya | Tidak | Ya |
| Menjadi type declaration | Ya | Ya | Tidak |
Panduan praktisnya: pakai interface untuk mendefinisikan kemampuan yang ingin dikenali sistem tipe, pakai inheritance ketika class benar-benar merupakan turunan dari parent yang berbagi banyak perilaku, dan pakai trait untuk membagikan potongan implementasi kecil ke class-class yang tidak berkerabat.
Ringkasan #
Trait memungkinkan pemakaian ulang implementasi secara horizontal: deklarasikan dengan trait, tempelkan dengan use, dan konflik antar trait diselesaikan lewat insteadof serta as. Trait boleh membawa property, method konkret, bahkan method abstract yang memaksa class pemakai melengkapi bagian tertentu, sementara constructor dalam trait sebaiknya dihindari karena bisa tertimpa secara diam-diam. Dengan memahami pembagian peran antara interface, inheritance, dan trait, kita kini siap mengenal satu bentuk class lagi yang lebih ekstrem: class tanpa nama sama sekali, yang kita bahas pada chapter anonymous classes berikutnya.