Trait

Trait #

PHP hanya mendukung pewarisan tunggal — satu kelas hanya bisa extend satu kelas induk. Ini berarti jika kamu memiliki dua kelas yang tidak berkerabat, misalnya User dan Produk, tapi keduanya membutuhkan fitur yang sama seperti timestamp otomatis, soft delete, atau kemampuan serialisasi, kamu tidak bisa berbagi kode itu lewat pewarisan tanpa memaksa hierarki yang tidak masuk akal. Trait adalah solusi PHP untuk masalah ini: mekanisme code reuse horizontal yang memungkinkan kamu menyisipkan method dan property ke dalam kelas manapun, terlepas dari hierarkinya. Artikel ini membahas trait secara mendalam — cara kerjanya, pola-pola nyata yang sering digunakan di framework seperti Laravel, cara menangani konflik saat dua trait punya method yang sama, dan — yang sama pentingnya — kapan trait justru menjadi masalah dan sebaiknya dihindari.

Mengapa Trait Ada — Masalah yang Dipecahkan #

Bayangkan tiga kelas yang tidak berkerabat tapi membutuhkan fitur yang sama:

flowchart TD
    A[Artikel] -- "butuh" --> T1[Timestamp\ncreated_at, updated_at]
    B[Produk] -- "butuh" --> T1
    C[Komentar] -- "butuh" --> T1
    A -- "butuh" --> T2[SoftDelete\ndeleted_at]
    B -- "butuh" --> T2
    D[User] -- "butuh" --> T3[HasUuid\nid sebagai UUID]
    B -- "butuh" --> T3

    style T1 fill:#dcfce7
    style T2 fill:#fef9c3
    style T3 fill:#dbeafe

Tanpa trait, ada dua pilihan yang sama-sama buruk: duplikasi kode di setiap kelas, atau memaksa hierarki pewarisan yang tidak natural (membuat BaseModel yang berisi semua fitur, padahal tidak semua model butuh semua fitur). Trait memberikan opsi ketiga: sisipkan hanya fitur yang dibutuhkan ke kelas yang membutuhkannya.


Mendefinisikan dan Menggunakan Trait #

Trait didefinisikan dengan keyword trait. Kelas menggunakannya dengan keyword use di dalam body kelas — bukan di atas file seperti use untuk namespace.

<?php
trait Timestampable
{
    private ?\DateTime $createdAt = null;
    private ?\DateTime $updatedAt = null;

    public function setCreatedAt(\DateTime $dt): void
    {
        $this->createdAt = $dt;
    }

    public function setUpdatedAt(\DateTime $dt): void
    {
        $this->updatedAt = $dt;
    }

    public function getCreatedAt(): ?\DateTime
    {
        return $this->createdAt;
    }

    public function getUpdatedAt(): ?\DateTime
    {
        return $this->updatedAt;
    }

    public function touch(): void
    {
        $now = new \DateTime();
        if ($this->createdAt === null) {
            $this->createdAt = $now;
        }
        $this->updatedAt = $now;
    }
}

// Dua kelas yang tidak berkerabat, keduanya pakai trait yang sama
class Artikel
{
    use Timestampable;

    public function __construct(
        private string $judul,
        private string $isi,
    ) {
        $this->touch(); // dari trait
    }
}

class Produk
{
    use Timestampable;

    public function __construct(
        private string $nama,
        private float  $harga,
    ) {
        $this->touch(); // dari trait — method yang sama, kelas berbeda
    }
}

$artikel = new Artikel("Belajar PHP", "Isi artikel...");
$produk  = new Produk("Laptop", 15_000_000);

echo $artikel->getCreatedAt()->format('Y-m-d H:i:s'); // waktu sekarang
echo $produk->getUpdatedAt()->format('Y-m-d H:i:s');  // waktu sekarang

Setelah trait di-use, semua method dan property-nya menjadi seolah-olah didefinisikan langsung di kelas tersebut. Tidak ada perbedaan dari luar — pemanggil tidak tahu apakah touch() berasal dari trait atau dari kelas itu sendiri.


Trait dengan Property #

Trait bisa mendefinisikan property, tapi ada satu aturan penting: jika kelas yang menggunakan trait juga mendefinisikan property dengan nama yang sama, tipe dan visibility-nya harus identik, jika tidak PHP melempar error.

<?php
trait HasUuid
{
    private string $uuid;

    public function initUuid(): void
    {
        // Buat UUID v4 sederhana
        $this->uuid = sprintf(
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }

    public function getUuid(): string
    {
        return $this->uuid;
    }
}

trait SoftDeletable
{
    private ?\DateTime $deletedAt = null;

    public function hapus(): void
    {
        $this->deletedAt = new \DateTime();
    }

    public function pulihkan(): void
    {
        $this->deletedAt = null;
    }

    public function sudahDihapus(): bool
    {
        return $this->deletedAt !== null;
    }

    public function getDeletedAt(): ?\DateTime
    {
        return $this->deletedAt;
    }
}

// Gunakan beberapa trait sekaligus
class User
{
    use HasUuid, Timestampable, SoftDeletable;

    public function __construct(
        private string $nama,
        private string $email,
    ) {
        $this->initUuid();
        $this->touch();
    }
}

$user = new User("Budi Santoso", "[email protected]");

echo $user->getUuid();                             // xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
echo $user->getCreatedAt()->format('Y-m-d');       // hari ini
echo $user->sudahDihapus() ? "dihapus" : "aktif"; // aktif

$user->hapus();
echo $user->sudahDihapus() ? "dihapus" : "aktif"; // dihapus

$user->pulihkan();
echo $user->sudahDihapus() ? "dihapus" : "aktif"; // aktif

Trait dengan Abstract Method #

Trait bisa mendefinisikan method abstract yang memaksa kelas yang menggunakannya untuk menyediakan implementasi. Ini berguna ketika trait butuh data dari kelas tapi tidak tahu bagaimana cara mendapatkannya:

<?php
trait Serializable
{
    // Trait memaksa kelas untuk menyediakan ini
    abstract protected function getAtributSerializable(): array;

    public function toArray(): array
    {
        $hasil = [];
        foreach ($this->getAtributSerializable() as $atribut) {
            $hasil[$atribut] = $this->$atribut ?? null;
        }
        return $hasil;
    }

    public function toJson(int $flags = JSON_UNESCAPED_UNICODE): string
    {
        return json_encode($this->toArray(), $flags);
    }

    public static function fromArray(array $data): static
    {
        $obj = new static();
        foreach ($data as $kunci => $nilai) {
            if (property_exists($obj, $kunci)) {
                $obj->$kunci = $nilai;
            }
        }
        return $obj;
    }
}

class Produk
{
    use Serializable;

    public function __construct(
        private int    $id    = 0,
        private string $nama  = '',
        private float  $harga = 0,
        private int    $stok  = 0,
    ) {}

    // Implementasikan method abstract dari trait
    protected function getAtributSerializable(): array
    {
        return ['id', 'nama', 'harga', 'stok'];
    }
}

$produk = new Produk(1, 'Laptop', 15_000_000, 5);
echo $produk->toJson();
// {"id":1,"nama":"Laptop","harga":15000000,"stok":5}

$dariArray = Produk::fromArray(['id' => 2, 'nama' => 'Mouse', 'harga' => 250000]);
echo $dariArray->toJson();
// {"id":2,"nama":"Mouse","harga":250000,"stok":0}

Mengatasi Konflik Method #

Ketika dua trait yang di-use oleh kelas yang sama memiliki method dengan nama identik, PHP tidak tahu method mana yang harus dipakai dan melempar fatal error. Ada dua cara menyelesaikannya: insteadof untuk memilih salah satu, dan as untuk membuat alias:

<?php
trait LoggerA
{
    public function log(string $pesan): void
    {
        echo "[A] $pesan\n";
    }

    public function debug(string $pesan): void
    {
        echo "[A-DEBUG] $pesan\n";
    }
}

trait LoggerB
{
    public function log(string $pesan): void
    {
        echo "[B] $pesan\n";
    }

    public function debug(string $pesan): void
    {
        echo "[B-DEBUG] $pesan\n";
    }
}

class Aplikasi
{
    use LoggerA, LoggerB {
        // insteadof — pilih LoggerA::log, abaikan LoggerB::log
        LoggerA::log     insteadof LoggerB;

        // insteadof — pilih LoggerB::debug, abaikan LoggerA::debug
        LoggerB::debug   insteadof LoggerA;

        // as — buat alias untuk method yang "dikalahkan"
        // sehingga masih bisa diakses dengan nama berbeda
        LoggerB::log     as logB;
        LoggerA::debug   as debugA;
    }
}

$app = new Aplikasi();
$app->log("pesan utama");  // [A] pesan utama — LoggerA menang
$app->logB("via alias");   // [B] via alias   — LoggerB via alias
$app->debug("debug info"); // [B-DEBUG] debug info — LoggerB menang
$app->debugA("detail");    // [A-DEBUG] detail — LoggerA via alias

Mengubah Visibility dengan as #

as juga bisa digunakan untuk mengubah visibility method dari trait:

<?php
trait FormatterTrait
{
    public function formatRupiah(float $nilai): string
    {
        return 'Rp ' . number_format($nilai, 0, ',', '.');
    }

    public function formatPersen(float $nilai): string
    {
        return number_format($nilai * 100, 1) . '%';
    }
}

class LaporanKeuangan
{
    use FormatterTrait {
        // Ubah visibility — method jadi protected, hanya untuk turunan
        formatRupiah as protected;

        // Buat alias dengan visibility berbeda
        formatPersen as public hitungPersen;
    }

    public function cetak(float $harga, float $diskon): void
    {
        // formatRupiah bisa dipanggil dari dalam kelas (protected OK)
        echo $this->formatRupiah($harga) . "\n";
        echo $this->hitungPersen($diskon) . "\n";
    }
}

$laporan = new LaporanKeuangan();
$laporan->cetak(15_000_000, 0.1);
// Rp 15.000.000
// 10.0%

// $laporan->formatRupiah(1000); // Fatal Error — sekarang protected
$laporan->hitungPersen(0.2);     // "20.0%" — alias tetap public

Trait Menggunakan Trait Lain #

Trait bisa menggunakan trait lain — ini memungkinkan komposisi trait yang lebih modular:

<?php
trait HasTimestamp
{
    private ?\DateTime $createdAt = null;
    private ?\DateTime $updatedAt = null;

    public function touchTimestamp(): void
    {
        $now = new \DateTime();
        $this->createdAt ??= $now;
        $this->updatedAt   = $now;
    }

    public function getCreatedAt(): ?\DateTime { return $this->createdAt; }
    public function getUpdatedAt(): ?\DateTime { return $this->updatedAt; }
}

trait HasSoftDelete
{
    private ?\DateTime $deletedAt = null;

    public function softDelete(): void  { $this->deletedAt = new \DateTime(); }
    public function restore(): void     { $this->deletedAt = null; }
    public function isTrashed(): bool   { return $this->deletedAt !== null; }
}

// Trait gabungan — menggunakan dua trait sekaligus
trait ModelTrait
{
    use HasTimestamp, HasSoftDelete;

    // Tambah method yang menggabungkan keduanya
    public function toMeta(): array
    {
        return [
            'created_at' => $this->createdAt?->format('Y-m-d H:i:s'),
            'updated_at' => $this->updatedAt?->format('Y-m-d H:i:s'),
            'deleted_at' => $this->deletedAt?->format('Y-m-d H:i:s'),
        ];
    }
}

// Kelas cukup gunakan satu trait, tapi dapat semua fitur
class Artikel
{
    use ModelTrait;

    public function __construct(private string $judul)
    {
        $this->touchTimestamp();
    }
}

$artikel = new Artikel("Belajar Trait PHP");
print_r($artikel->toMeta());
// Array ( [created_at] => 2024-01-15 10:30:00 [updated_at] => ... [deleted_at] => )

Pola Trait yang Umum di Framework #

Framework PHP seperti Laravel menggunakan trait secara ekstensif. Berikut beberapa pola yang paling sering ditemui:

Pola: Singleton via Trait #

<?php
trait Singleton
{
    private static ?self $instance = null;

    // Cegah instantiasi langsung
    private function __construct() {}
    private function __clone() {}

    public static function getInstance(): static
    {
        if (static::$instance === null) {
            static::$instance = new static();
        }
        return static::$instance;
    }
}

class Konfigurasi
{
    use Singleton;

    private array $data = [];

    public function set(string $kunci, mixed $nilai): void
    {
        $this->data[$kunci] = $nilai;
    }

    public function get(string $kunci, mixed $default = null): mixed
    {
        return $this->data[$kunci] ?? $default;
    }
}

$config1 = Konfigurasi::getInstance();
$config2 = Konfigurasi::getInstance();

$config1->set('debug', true);
echo $config2->get('debug') ? 'true' : 'false'; // true — instance yang sama
var_dump($config1 === $config2);                  // bool(true)

Pola: Observable via Trait #

<?php
trait Observable
{
    private array $listeners = [];

    public function on(string $event, callable $callback): void
    {
        $this->listeners[$event][] = $callback;
    }

    protected function emit(string $event, mixed ...$args): void
    {
        foreach ($this->listeners[$event] ?? [] as $callback) {
            $callback(...$args);
        }
    }
}

class Order
{
    use Observable;

    private string $status = 'pending';

    public function setStatus(string $status): void
    {
        $statusLama   = $this->status;
        $this->status = $status;

        // Emit event — semua listener akan dipanggil
        $this->emit('statusChanged', $statusLama, $status, $this);
    }

    public function getStatus(): string
    {
        return $this->status;
    }
}

$order = new Order();

// Daftarkan listener
$order->on('statusChanged', function(string $lama, string $baru, Order $order) {
    echo "Order berubah dari '$lama' ke '$baru'\n";
});

$order->on('statusChanged', function(string $lama, string $baru, Order $order) {
    // Kirim notifikasi email, log, dll.
    echo "Mengirim notifikasi untuk status: $baru\n";
});

$order->setStatus('processing');
// Order berubah dari 'pending' ke 'processing'
// Mengirim notifikasi untuk status: processing

$order->setStatus('shipped');
// Order berubah dari 'processing' ke 'shipped'
// Mengirim notifikasi untuk status: shipped

Pola: Method Chaining via Trait #

<?php
trait Fluent
{
    // Memungkinkan method chaining dengan mengembalikan $this
    protected function set(string $property, mixed $nilai): static
    {
        $this->$property = $nilai;
        return $this;
    }
}

class QueryBuilder
{
    use Fluent;

    private string  $tabel    = '';
    private array   $kondisi  = [];
    private ?int    $batas    = null;
    private ?int    $offset   = null;
    private string  $urutan   = '';

    public function from(string $tabel): static
    {
        return $this->set('tabel', $tabel);
    }

    public function where(string $kondisi): static
    {
        $this->kondisi[] = $kondisi;
        return $this;
    }

    public function limit(int $batas): static
    {
        return $this->set('batas', $batas);
    }

    public function offset(int $offset): static
    {
        return $this->set('offset', $offset);
    }

    public function orderBy(string $kolom, string $arah = 'ASC'): static
    {
        return $this->set('urutan', "$kolom $arah");
    }

    public function build(): string
    {
        $sql = "SELECT * FROM {$this->tabel}";

        if (!empty($this->kondisi)) {
            $sql .= " WHERE " . implode(' AND ', $this->kondisi);
        }
        if ($this->urutan) {
            $sql .= " ORDER BY {$this->urutan}";
        }
        if ($this->batas !== null) {
            $sql .= " LIMIT {$this->batas}";
        }
        if ($this->offset !== null) {
            $sql .= " OFFSET {$this->offset}";
        }

        return $sql;
    }
}

$query = (new QueryBuilder())
    ->from('produk')
    ->where('stok > 0')
    ->where('harga < 5000000')
    ->orderBy('harga', 'ASC')
    ->limit(10)
    ->offset(0)
    ->build();

echo $query;
// SELECT * FROM produk WHERE stok > 0 AND harga < 5000000 ORDER BY harga ASC LIMIT 10 OFFSET 0

Trait vs Interface vs Abstract Class #

Ketiganya bisa terlihat serupa di permukaan, tapi masing-masing punya peran yang berbeda:

AspekTraitInterfaceAbstract Class
Bisa di-instantiate
Berisi implementasi✓ (sebagian)
Berisi property
Berisi konstanta✓ (PHP 8.2+)
Jumlah yang bisa dipakaiBanyakBanyakSatu
Membentuk tipe (instanceof)
MerepresentasikanKemampuan yang di-copyKontrak / tipeTipe dasar + implementasi bersama

Perbedaan paling kritis: trait tidak membentuk tipe. Kelas yang menggunakan Timestampable trait tidak bisa di-type hint sebagai Timestampable. Untuk itu, kamu perlu interface:

<?php
// Trait untuk implementasi — method yang di-copy ke kelas
trait TimestampableTrait
{
    private ?\DateTime $createdAt = null;

    public function touch(): void
    {
        $this->createdAt ??= new \DateTime();
    }

    public function getCreatedAt(): ?\DateTime
    {
        return $this->createdAt;
    }
}

// Interface untuk kontrak — type hint
interface TimestampableInterface
{
    public function touch(): void;
    public function getCreatedAt(): ?\DateTime;
}

// Gabungkan keduanya — paling lengkap
class Artikel implements TimestampableInterface
{
    use TimestampableTrait; // implementasi dari trait
    // kontrak dari interface — tidak perlu tulis ulang karena trait sudah implement
}

// Sekarang bisa di-type hint!
function prosesTimestamp(TimestampableInterface $entitas): void
{
    $entitas->touch();
    echo $entitas->getCreatedAt()->format('Y-m-d') . "\n";
}

prosesTimestamp(new Artikel()); // bekerja karena Artikel implements interface

Kapan Trait Tepat dan Kapan Dihindari #

Gunakan Trait jika:
  ✓ Logika yang sama perlu ada di kelas-kelas yang tidak berkerabat
  ✓ Fitur bersifat "horizontal" — bukan relasi is-a, tapi has-behavior
  ✓ Implementasi cukup stabil dan tidak perlu di-swap/diganti
  ✓ Contoh nyata: Timestampable, SoftDelete, HasUuid, Observable, Singleton

Hindari Trait jika:
  ✗ Implementasinya perlu diganti-ganti (gunakan interface + injection)
  ✗ Trait sangat bergantung pada property kelas yang memakainya — coupling tersembunyi
  ✗ Kelas menggunakan terlalu banyak trait sehingga sulit tahu method dari mana
  ✗ Sebenarnya ini adalah relasi is-a — gunakan pewarisan
  ✗ Ingin membentuk tipe untuk type hint — gunakan interface
<?php
// ANTI-PATTERN: trait yang bergantung pada property kelas tanpa kontrak
trait PerhitunganHarga
{
    public function hitungTotal(): float
    {
        // Asumsi $this->harga dan $this->qty ada di kelas — tidak ada jaminan!
        return $this->harga * $this->qty * (1 + $this->pajak);
    }
}

class Tagihan
{
    use PerhitunganHarga;
    // Bagaimana tahu $harga, $qty, $pajak harus ada? Tidak ada dokumentasi jelas
}

// BENAR: deklarasikan property yang dibutuhkan di dalam trait sendiri,
// atau gunakan abstract method untuk memaksanya
trait PerhitunganHargaV2
{
    abstract protected function getHarga(): float;
    abstract protected function getQty(): int;
    abstract protected function getPajak(): float;

    public function hitungTotal(): float
    {
        return $this->getHarga() * $this->getQty() * (1 + $this->getPajak());
    }
}

Ringkasan #

  • Trait adalah mekanisme reuse horizontal — menyisipkan method dan property ke kelas manapun tanpa pewarisan. Paling berguna untuk kemampuan yang dibutuhkan kelas-kelas yang tidak berkerabat dalam hierarki.
  • Trait tidak membentuk tipe — kelas yang use Timestampable tidak otomatis menjadi instance Timestampable. Untuk type hint, kombinasikan trait (implementasi) dengan interface (kontrak).
  • Beberapa trait bisa digunakan sekaligususe TraitA, TraitB, TraitC;. Trait juga bisa menggunakan trait lain untuk membangun komposisi yang lebih modular.
  • Konflik method diselesaikan dengan insteadof (pilih satu) dan as (buat alias untuk yang “dikalahkan”). as juga bisa digunakan untuk mengubah visibility method.
  • Abstract method di trait memaksa kelas yang menggunakan trait menyediakan implementasi tertentu — cara yang bersih untuk membuat trait bergantung pada kontrak yang jelas, bukan asumsi terhadap property kelas.
  • Pola trait yang umum di framework: Singleton, Observable/Event, Timestampable, SoftDelete, HasUuid, dan Fluent method chaining.
  • Hindari trait ketika: implementasi perlu diganti-ganti (gunakan interface), trait punya terlalu banyak asumsi tersembunyi tentang kelas yang memakainya, atau sebenarnya ini adalah relasi is-a yang lebih tepat diekspresikan lewat pewarisan.
  • Trait + Interface adalah kombinasi terkuat: trait menyediakan implementasi yang di-copy ke kelas, interface menyediakan kontrak yang bisa dipakai untuk type hint dan dependency injection.

← Sebelumnya: Interface   Berikutnya: Eksepsi →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact