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
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:
protected function name()— nama method sama persis dengan nama kolomname; Eloquent otomatis menghubungkan keduanya.get:— closure yang menerima nilai mentah dari database dan mengembalikan nilai yang dibaca aplikasi. Di siniucfirst()mengkapitalkan huruf pertama.set:— closure yang menerima nilai dari aplikasi dan mengembalikan nilai yang disimpan. Di sinitrim()memangkas spasi berlebih di awal dan akhir.
Mari kita buktikan kerjanya dua arah dengan membuat user bernama ' budi ' (dengan spasi):
$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:
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:
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:
$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:
slug yang ditulis : Belajar Accessor dan Mutator
slug di database : belajar-accessor-dan-mutatorStr::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:
use Illuminate\Support\Facades\Hash;
protected function password(): Attribute
{
return Attribute::make(
set: fn (string $value) => Hash::make($value),
);
}$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:
password di database (7 karakter pertama): $2y$12$...
Hash::check('rahasia-kuat', ...) : true
Hash::check('salah', ...) : falsePassword 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:
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():
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:
$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:
// 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.