Масове призначення - заповнення моделі масивом: User::create($data), $user->update($data), $user->fill($data). Якщо масив прийшов із запиту без фільтрації, користувач може змінити поля, яких у формі немає:
$user->update($request->all());
// POST name=Оля&is_admin=1 → користувач став адміністратором
$fillable - білий список полів, які можна призначати масово:
class User extends Authenticatable
{
protected $fillable = ['name', 'email', 'password'];
}
Поля поза списком при масовому призначенні мовчки ігноруються.
$guarded - чорний список: усе, крім указаних. protected $guarded = []; вимикає захист повністю.
Чому $fillable безпечніший: нова колонка в таблиці (is_admin, balance, email_verified_at) за $guarded автоматично стає доступною для масового призначення, а за $fillable - ні, доки її свідомо не додадуть.
Сучасний варіант - атрибути класу:
#[Fillable(['name', 'email', 'password'])]
class User extends Authenticatable {}
Головний захист - не $fillable, а валідація:
$user->update($request->validated());
validated() повертає лише поля, для яких є правила. $fillable - друга лінія оборони на випадок, коли хтось передасть $request->all().
Поля, які змінюються лише за правами (роль, статус модерації, баланс), не повинні бути у $fillable взагалі - їх змінює окремий код з перевіркою прав:
$user->forceFill(['role' => 'editor'])->save(); // явно, у коді адміністратора
Корисна перевірка в розробці:
Model::preventSilentlyDiscardingAttributes(! app()->isProduction());
Тепер спроба масово призначити поле поза $fillable кидає виняток замість мовчазного ігнорування - помилки в коді помітні одразу, а не в продакшені.
Model::unguard() вимикає захист глобально - допустимо в сидерах, але не в коді, що обробляє запити.