Database menyimpan data mentah, sedangkan aplikasi sering butuh data dalam bentuk yang lebih manusiawi: nama tampil dengan huruf kapital di awal, slug terbentuk otomatis dari judul, password tersimpan sebagai hash, harga tampil berformat rupiah. Kita bisa menulis transformasi ini di setiap controller — atau lebih baik, menempelkannya langsung pada kolom di model. Accessor mengubah nilai saat dibaca, mutator mengubah nilai saat disimpan. Sejak Laravel 9 keduanya ditulis lewat class Attribute dengan named argument get: dan set:, satu method per kolom. Chapter ini membedah sintaks modern tersebut beserta atribut virtual dan $appends.

Sintaks Modern: Attribute::make() #

Setiap accessor/mutator adalah method protected di model yang bernama sama dengan kolomnya, mengembalikan Attribute::make(...). Contoh pertama kita: kolom name pada model User yang otomatis dirapikan dua arah:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    // ... fillable, factory, dst.

    protected function name(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucfirst($value),
            set: fn (string $value) => trim($value),
        );
    }
}

Tiga bagian penting dari definisi ini:

  1. protected function name() — nama method sama persis dengan nama kolom name; Eloquent otomatis menghubungkan keduanya.
  2. get: — closure yang menerima nilai mentah dari database dan mengembalikan nilai yang dibaca aplikasi. Di sini ucfirst() mengkapitalkan huruf pertama.
  3. set: — closure yang menerima nilai dari aplikasi dan mengembalikan nilai yang disimpan. Di sini trim() memangkas spasi berlebih di awal dan akhir.

Mari kita buktikan kerjanya dua arah dengan membuat user bernama ' budi ' (dengan spasi):

php
$user = User::create([
    'name' => ' budi ',
    'email' => '[email protected]',
    'password' => 'rahasia-kuat',
]);

echo 'name yang dibaca    : ', $user->name, "\n";
echo 'name di database    : ', var_export(DB::table('users')
    ->where('id', $user->id)->value('name'), true), "\n";
echo "getRawOriginal('name'): ", var_export($user->getRawOriginal('name'), true), "\n";

Output yang Dihasilkan:

text
name yang dibaca    : Budi
name di database    : 'budi'
getRawOriginal('name'): 'budi'

Nilai yang ditulis ' budi ' tersimpan sebagai 'budi' (spasi terpangkas oleh set:), lalu terbaca sebagai 'Budi' (kapital oleh get:). Method getRawOriginal() memperlihatkan nilai asli di database tanpa accessor — berguna saat kita butuh data mentah di tengah transformasi.

Mutator untuk Slug Otomatis #

Mutator bisa berdiri sendiri tanpa get:. Pola yang sangat umum: men-generate slug setiap kali kolom slug diisi, sehingga pemanggil cukup menulis judulnya saja:

php
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Str;

protected function slug(): Attribute
{
    return Attribute::make(
        set: fn (string $value) => Str::slug($value),
    );
}

Kita uji dengan menulis slug berupa judul asli yang belum diproses:

php
$post = Post::create([
    'user_id' => $user->id,
    'judul' => 'Belajar Accessor dan Mutator',
    'slug' => 'Belajar Accessor dan Mutator',
    'isi' => 'Contoh mutator slug.',
    'views' => 0,
    'harga' => 150000,
    'published' => false,
]);

echo 'slug yang ditulis  : Belajar Accessor dan Mutator', "\n";
echo 'slug di database   : ', DB::table('posts')
    ->where('id', $post->id)->value('slug'), "\n";

Output yang Dihasilkan:

text
slug yang ditulis  : Belajar Accessor dan Mutator
slug di database   : belajar-accessor-dan-mutator

Str::slug() mengubah teks menjadi huruf kecil dengan kata dipisah tanda hubung. Berapa pun tempat yang membuat post, format slug dijamin konsisten karena logikanya menempel di model, bukan di controller masing-masing.

Mutator Hashing Password #

Contoh mutator klasik lainnya adalah meng-hash password saat disimpan. Secara teknis bentuknya sama seperti slug — hanya beda fungsi:

php
use Illuminate\Support\Facades\Hash;

protected function password(): Attribute
{
    return Attribute::make(
        set: fn (string $value) => Hash::make($value),
    );
}
php
$rawPassword = DB::table('users')->where('id', $user->id)->value('password');
echo 'password di database (7 karakter pertama): ', substr($rawPassword, 0, 7), "...\n";
echo 'Hash::check(\'rahasia-kuat\', ...) : ', Hash::check('rahasia-kuat', $rawPassword) ? 'true' : 'false', "\n";
echo 'Hash::check(\'salah\', ...)        : ', Hash::check('salah', $rawPassword) ? 'true' : 'false', "\n";

Output yang Dihasilkan:

text
password di database (7 karakter pertama): $2y$12$...
Hash::check('rahasia-kuat', ...) : true
Hash::check('salah', ...)        : false

Password asli rahasia-kuat tidak pernah tersimpan — database hanya memuat hash bcrypt (selalu berawalan $2y$12$ pada konfigurasi bawaan), dan Hash::check() berhasil mencocokkannya.

Atribut Virtual dan $appends #

Accessor juga bisa mendefinisikan atribut yang tidak ada di database — nilainya dihitung dari kolom lain. Atribut semacam ini disebut computed attribute. Berikut harga_rupiah yang memformat kolom harga:

php
use Illuminate\Database\Eloquent\Casts\Attribute;

protected $appends = ['harga_rupiah'];

protected function hargaRupiah(): Attribute
{
    return Attribute::make(
        get: fn () => $this->harga === null
            ? null
            : 'Rp'.number_format((float) $this->harga, 2, ',', '.'),
    );
}

Perhatikan dua hal. Method bernama hargaRupiah (camelCase) otomatis dipetakan ke atribut harga_rupiah (snake_case) — kebalikan dari aturan kolom. Closure get: tidak menerima $value karena memang tidak ada kolom sumbernya; nilainya dihitung dari $this->harga. Property $appends kemudian memasukkan atribut virtual ini ke hasil toArray() dan toJson():

php
echo '$post->harga_rupiah: ', $post->harga_rupiah, "\n";
echo 'Post #2 (harga NULL): ', var_export(Post::find(2)->harga_rupiah, true), "\n";
echo 'Kunci toArray(): ', implode(', ', array_keys($post->toArray())), "\n";
echo json_encode($post->only(['judul', 'harga', 'harga_rupiah'])), "\n";

Output yang Dihasilkan:

text
$post->harga_rupiah: Rp150.000,00
Post #2 (harga NULL): NULL
Kunci toArray(): user_id, judul, slug, isi, views, harga, published, updated_at, created_at, id, harga_rupiah
{"judul":"Belajar Accessor dan Mutator","harga":150000,"harga_rupiah":"Rp150.000,00"}

Tanpa $appends, harga_rupiah tetap bisa diakses sebagai $post->harga_rupiah, tetapi tidak ikut saat model diserialisasi ke array atau JSON — misalnya untuk response API. Property $appends inilah yang membuatnya hadir di output serialisasi tersebut.

Sintaks Lama: getFooAttribute #

Sebelum Laravel 9, accessor dan mutator ditulis sebagai method terpisah dengan pola nama get/set + kolom + Attribute:

php
// Gaya lama (Laravel 8 dan sebelumnya) — masih didukung
public function getJudulSingkatAttribute()
{
    return Str::limit($this->judul, 15);
}

public function setNameAttribute($value)
{
    $this->attributes['name'] = trim($value);
}

Method pertama dipanggil sebagai $post->judul_singkat, method kedua menyala otomatis saat name diisi. Kode warisan masih banyak memakai pola ini, jadi kenali bentuknya — namun untuk kode baru, dokumentasi Eloquent: Mutators menyarankan class Attribute yang lebih ringkas dan terkumpul dalam satu method.

Accessor dan mutator menuntut kita menulis transformasi secara manual satu per satu. Untuk kebutuhan yang paling umum — boolean, angka, tanggal, JSON — Laravel menyediakan cara yang lebih deklaratif: cukup daftarkan tipe kolom di $casts, dan konversinya berjalan sendiri. Itulah topik chapter berikutnya.