User::create($request->all()) виглядає зручно, поки не згадати, що запит формує клієнт. HTML-форма може мати три поля, але ніщо не заважає відправити POST руками з четвертим, is_admin=1, або з company_id чужої компанії. Якщо модель приймає масив цілком, Eloquent запише все, що збігається з колонками таблиці. Саме так у 2012 році Єгор Хомаков додав свій SSH-ключ до організації rails на GitHub і зробив коміт у репозиторій Rails, після чого Rails спершу закрив масове присвоєння за замовчуванням у нових застосунках, а в Rails 4 перейшов на strong parameters.
Eloquent закритий так само: модель без налаштувань має $guarded = ['*'], і create() з будь-яким полем кидає MassAssignmentException. Відкривають її білим списком $fillable (у Laravel 13 ще й атрибутом #[Fillable([...])]) або чорним $guarded. Білий список надійніший: нова колонка is_verified, додана міграцією через пів року, лишається закритою, поки її свідомо не додадуть. Із чорним списком вона відкрита, і про це ніхто не подумає. Поля поза $fillable за замовчуванням мовчки відкидаються, тому в розробці вмикають Model::preventSilentlyDiscardingAttributes(), щоб бачити такі місця винятком, а не загадковим «поле не зберігається».
Другий шар у контролері. $request->validated() повертає лише ключі, для яких у Form Request є правила, і дописане поле туди не потрапить навіть з $guarded = []. Але валідація перевіряє, чи значення коректне, а не чи має користувач право його змінювати. Поле role з правилом in:user,admin пройде перевірку зі значенням admin, тому для форми профілю і для адмінки потрібні різні запити з різними правилами. Вкладені масиви теж треба обмежувати: 'meta' => 'array' пропустить будь-які ключі в JSON-колонку, а 'meta' => 'array:city,remote' лише перелічені.
Поля, які визначає сервер, узагалі не мають приходити з форми. Власника ставлять через звʼязок ($request->user()->resumes()->create(...)), статус і роль присвоюють явно або через forceFill() зі значеннями, сформованими в коді. Тоді в $fillable лишаються лише поля, які користувач справді редагує сам, і дві помилки, mass assignment і підстановка чужого company_id, закриваються одним рішенням.
// Було: у модель летить усе, що прислав клієнт, зокрема is_admin=1.
$user->update($request->all());
// Модель: білий список полів для масового заповнення (атрибут Laravel 13).
#[Fillable(['name', 'city', 'about'])]
class User extends Authenticatable {}
// Form Request: лише поля, які користувач має право змінювати сам.
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'city' => ['nullable', 'string', 'max:60'],
'about' => ['nullable', 'string', 'max:2000'],
// 'role' тут немає: роль змінює адмінка іншим запитом з іншою перевіркою.
];
}
// Контролер: validated() замість all().
$request->user()->update($request->validated());
// Службові поля ставить сервер: власника через звʼязок, статус явно.
$vacancy = $company->vacancies()->make($request->validated());
$vacancy->status = VacancyStatus::Draft;
$vacancy->save();
// AppServiceProvider::boot(): у розробці падати на полях поза $fillable.
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
Скажіть, що захист має два незалежні шари: модель вирішує, які поля взагалі можна заповнювати масово, а запит вирішує, які поля прийшли від цього користувача. І окремо назвіть поля, які ніколи не приходять із форми: власник, роль, статус оплати.