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
<?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:

text
Artikel dibuat: 2026-01-15 08:00:00
Komentar dibuat: 2026-01-15 09:30:00

Mari kita bedah kode di atas:

  1. trait PenandaWaktu: mendefinisikan property $dibuatPada beserta method tandai() — keduanya akan tersedia bagi setiap class yang memakai trait ini.
  2. use PenandaWaktu;: statement di dalam body class menempelkan seluruh isi trait ke class Artikel dan Komentar.
  3. Dua class tanpa hubungan keluarga kini berbagi implementasi yang sama tanpa extends sama 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
<?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:

text
Selamat datang
Halo, apa kabar

Bagian 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
<?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:

text
Budi Santoso

Trait 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
<?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:

text
Harga setelah pajak: Rp111000

Method 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
<?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:

text
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
<?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:

text
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:

AspekInheritance (extends)Interface (implements)Trait (use)
Hubunganis-a (merupakan)bisa-melakukanmenempelkan implementasi
Arah pembagianVertikal, parent ke childKontrak lintas classHorizontal, lintas class
Jumlah per classSatu parentBanyak interfaceBanyak trait
Membawa kode konkretYaTidakYa
Menjadi type declarationYaYaTidak

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.

Referensi #