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:

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

text
MassAssignmentException: Add [user_id] to fillable property to allow mass assignment on [App\Models\Post].
jumlah post sekarang: 0

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

php
$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:

text
tersimpan: Belajar Mass Assignment | views: 0 | id: 1

Post 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 berpikirsebutkan yang bolehsebutkan yang dilarang
Kolom baru ditambahkan ke tabelotomatis terlindungiotomatis terbuka
Cocok untukmayoritas model aplikasimodel 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:

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

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

text
is_admin Rina di database: false

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

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

text
is_admin penyerang di database: true

Pada 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():

php
$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:

text
setelah fill(): id = NULL | jumlah post = 1
setelah save(): id = 2 | jumlah post = 2

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

php
$draft->forceFill(['views' => 7])->save();

echo 'views setelah forceFill: ', $draft->fresh()->views, "\n";

Output yang Dihasilkan:

text
views setelah forceFill: 7

Kolom 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.