Bayangkan form postingan dengan delapan kolom: judul, slug, isi, kategori, sampai status publish. Mengisi delapan $post->kolom = ... di controller adalah tugas yang membosankan dan mudah salah ketik. Mass assignment menyelesaikannya: kita menyerahkan satu array ke model, dan Eloquent memetakan setiap kunci array ke kolom yang sesuai. Kepraktisan seperti ini, entah bagaimana, juga membuka pintu bagi input asing untuk menyusup masuk — karena itu Eloquent melindunginya dengan mekanisme whitelist fillable. Chapter ini membahas cara kerja create(), fill(), dan forceFill(), kenapa MassAssignmentException justru kabar baik, serta bagaimana whitelist menyelamatkan kolom sensitif seperti is_admin.
Penolakan Pertama: MassAssignmentException #
Method Post::create(array $attributes) adalah pintu masuk mass assignment: satu panggilan, satu array, satu baris baru. Mari kita coba langsung pada model Post yang masih polos dari generator artisan:
use App\Models\Post;
try {
Post::create([
'user_id' => 1,
'judul' => 'Gagal Mass Assignment',
'slug' => 'gagal-mass-assignment',
'isi' => 'Baris ini seharusnya tidak pernah tersimpan.',
]);
} catch (Illuminate\Database\Eloquent\MassAssignmentException $e) {
echo 'MassAssignmentException: ', $e->getMessage(), "\n";
}
echo 'jumlah post sekarang: ', Post::count(), "\n";Output yang Dihasilkan:
MassAssignmentException: Add [user_id] to fillable property to allow mass assignment on [App\Models\Post].
jumlah post sekarang: 0Tidak ada baris tersimpan. Secara bawaan, model Eloquent berada dalam keadaan totally guarded: property $guarded berisi ['*'], artinya semua kolom ditolak untuk mass assignment. Penolakan ini bukan bug, melainkan posisi aman — Eloquent memaksa kita menyatakan lebih dulu kolom mana yang boleh diisi dari luar.
Whitelist dengan $fillable dan #[Fillable] #
Whitelist dideklarasikan dengan mendaftarkan kolom yang diizinkan. Di Laravel 13, stub model bawaan sudah memakai PHP attribute #[Fillable] — persis seperti yang dihasilkan artisan pada model User:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['user_id', 'judul', 'slug', 'isi', 'harga', 'published'])]
class Post extends Model
{
//
}Gaya klasiknya masih sepenuhnya didukung dan banyak dijumpai di proyek lama: property protected $fillable = ['user_id', 'judul', 'slug', 'isi', 'harga', 'published']; menghasilkan efek yang sama. Perhatikan bahwa views sengaja tidak kita masukkan ke whitelist — ini akan menjadi bahan pembuktian. Dengan whitelist aktif, pemanggilan create() yang tadi gagal kini berjalan:
$post = Post::create([
'user_id' => 1,
'judul' => 'Belajar Mass Assignment',
'slug' => 'belajar-mass-assignment',
'isi' => 'Tersimpan karena semua kolom masuk whitelist.',
'views' => 99999,
]);
$fresh = $post->fresh();
echo 'tersimpan: ', $fresh->judul, ' | views: ', $fresh->views, ' | id: ', $fresh->id, "\n";Output yang Dihasilkan:
tersimpan: Belajar Mass Assignment | views: 0 | id: 1Post tersimpan, tetapi lihat angka views: kita mengirim 99999, dan database menyimpan 0 — nilai default kolom. Kolom di luar whitelist tidak melempar exception, melainkan diabaikan secara diam-diam. Inilah semantik whitelist: yang terdaftar diisi, sisanya dibuang.
$fillable versus $guarded #
Selain whitelist, Eloquent juga mengenal blacklist lewat $guarded — kolom yang dilarang diisi, sementara sisanya bebas:
$fillable (whitelist) | $guarded (blacklist) | |
|---|---|---|
| Cara berpikir | sebutkan yang boleh | sebutkan yang dilarang |
| Kolom baru ditambahkan ke tabel | otomatis terlindungi | otomatis terbuka |
| Cocok untuk | mayoritas model aplikasi | model kecil yang seluruh kolomnya aman |
| Kasus khusus | [] berarti semua tertutup | [] berarti semua terbuka |
Blacklist rapuh terhadap perubahan skema: setiap kolom baru yang sensitif harus diingat untuk didaftarkan sebagai terlarang. Praktik industri umumnya memilih $fillable per model. Kebalikannya, $guarded = [] membuka semua kolom dan hanya pantas untuk model data internal yang tidak pernah disentuh input pengguna.
Serangan is_admin dan Senyapnya Penyelamat #
Untuk melihat whitelist bekerja pada kasus nyata, kita pakai model User bawaan Laravel yang whitelist-nya berisi name, email, dan password, sementara tabel users kita tambahi kolom is_admin lewat migration:
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false);
});Skenarionya klasik: form pendaftaran yang datanya dimasukkan pengguna, lalu seorang penyerang menambahkan field is_admin ke request:
use App\Models\User;
$user = User::create([
'name' => 'Rina',
'email' => '[email protected]',
'password' => bcrypt('password'),
'is_admin' => true,
]);
echo 'is_admin Rina di database: ', $user->fresh()->is_admin ? 'true' : 'false', "\n";Output yang Dihasilkan:
is_admin Rina di database: falseField is_admin lenyap dibuang whitelist sebelum menyentuh query, sehingga kolom itu memakai nilai default false dari database. Bandingkan dengan dunia tanpa proteksi — mode unguard() mematikan seluruh mekanisme mass assignment secara global:
User::unguard();
$penyerang = User::create([
'name' => 'Attacker',
'email' => '[email protected]',
'password' => bcrypt('password'),
'is_admin' => true,
]);
User::reguard();
echo 'is_admin penyerang di database: ', $penyerang->fresh()->is_admin ? 'true' : 'false', "\n";Output yang Dihasilkan:
is_admin penyerang di database: truePada mode unguarded, is_admin => true lolos masuk dan penyerang mendapatkan hak admin. Kerentanan semacam ini pernah jadi kelas bug yang lazim di era framework awal, ketika kebiasaan yang beredar adalah menyerahkan Input::all() mentah-mentah ke model tanpa penyaringan — pengalaman itulah yang membuat Laravel mengunci totally guarded sebagai bawaan. Jadi aturannya sederhana: whitelist setiap model, dan jangan pernah menyerahkan data mentah request tanpa melalui validasi lebih dulu.
fill() dan forceFill() #
Dua method pelengkap mengisi atribut dengan proteksi yang berbeda. Method fill() mengisi atribut di memori tanpa menyimpan — pas untuk membangun object bertahap sebelum save():
$draft = new Post;
$draft->fill([
'user_id' => 1,
'judul' => 'Draft Belum Tersimpan',
'slug' => 'draft-belum-tersimpan',
'isi' => 'Masih hidup di memori.',
]);
echo 'setelah fill(): id = ', var_export($draft->id, true),
' | jumlah post = ', Post::count(), "\n";
$draft->save();
echo 'setelah save(): id = ', $draft->id,
' | jumlah post = ', Post::count(), "\n";Output yang Dihasilkan:
setelah fill(): id = NULL | jumlah post = 1
setelah save(): id = 2 | jumlah post = 2Sebelum save(), object masih tanpa id dan jumlah baris tetap satu — fill() murni bekerja di memori, tunduk pada whitelist yang sama dengan create(). Sebaliknya, forceFill() mengisi atribut melewati whitelist sepenuhnya:
$draft->forceFill(['views' => 7])->save();
echo 'views setelah forceFill: ', $draft->fresh()->views, "\n";Output yang Dihasilkan:
views setelah forceFill: 7Kolom views yang tadi dibuang diam-diam kini berhasil diisi — forceFill() memang menembus proteksi. Detail selengkapnya tentang fillable, guarded, dan kawan-kawannya ada di dokumentasi Eloquent
.
Dengan create() yang aman dan fill() yang terkendali, model Post kini siap menerima data dari luar. Satu pertanyaan berikutnya: bagaimana cara membaca data itu kembali dengan rapi? Chapter berikutnya membahas all(), find(), where(), dan kawan-kawan retrieval.