[{"data":1,"prerenderedAt":6},["ShallowReactive",2],{"post-content-laravel-mass-assignment-fillable-error-fix":3},{"content":4,"lastModified":5},"\u003Ch2>The error I inherited\u003C\u002Fh2>\n\u003Cp>Last month I opened a client admin panel that has been running since 2023, added one field to a signup form, and immediately got hit with \u003Ccode>Illuminate\\Database\\Eloquent\\MassAssignmentException: Add [role] to fillable property to allow mass assignment on [App\\Models\\User]\u003C\u002Fcode>. Every Laravel developer meets this error in their first week. What made me actually stop was the model itself. A previous developer had commented out \u003Ccode>$fillable\u003C\u002Fcode>, set \u003Ccode>$guarded = []\u003C\u002Fcode>, and left a comment above it that said \u003Cem>fixed\u003C\u002Fem>. It had been running that way for three years.\u003C\u002Fp>\n\u003Cp>That comment is why I'm writing this. The exception is not Laravel being difficult. It is one of the few places where the framework refuses to run code that would work, and the workarounds people use to silence it are the actual bug.\u003C\u002Fp>\n\n\u003Ch2>What mass assignment is\u003C\u002Fh2>\n\u003Cp>When you pass an array straight into \u003Ccode>create()\u003C\u002Fcode>, \u003Ccode>fill()\u003C\u002Fcode> or \u003Ccode>update()\u003C\u002Fcode>, every key in that array becomes a column write. If the array comes from the request, the client decides what's in it.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-php\">\u002F\u002F The line every beginner writes\n$user = User::create($request-&gt;all());\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Now imagine the users table has an \u003Ccode>is_admin\u003C\u002Fcode> column. Nothing stops a request from containing it:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-text\">POST \u002Fregister\nname=Ada&amp;email=ada@example.com&amp;password=s3cret&amp;role=admin\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Without the guard, that POST creates an administrator. With it, Eloquent throws instead of writing the field. The error message names the exact key it refused, which is a nice touch. It is telling you that unvetted input reached a model, not that your syntax is wrong.\u003C\u002Fp>\n\n\u003Ch2>The fix, and the line that makes it stick\u003C\u002Fh2>\n\u003Cp>Whitelist the fields the form is allowed to set, and stop passing the raw request:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-php\">class User extends Model\n{\n    protected $fillable = ['name', 'email', 'password'];\n}\n\n\u002F\u002F in the controller\n$user = User::create($request-&gt;validated());\n$user-&gt;role = 'admin'; \u002F\u002F direct assignment skips fillable, by design\n$user-&gt;save();\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Two details here matter more than they look. First, \u003Ccode>validated()\u003C\u002Fcode> returns only the keys your validation rules covered, so the whole class of stray-input problems dies before it reaches the model. Second, assigning \u003Ccode>role\u003C\u002Fcode> directly is not a workaround. It's the intended path for fields no user input should ever set. Privileged writes stay visible in the diff.\u003C\u002Fp>\n\n\u003Ch2>The silent version is worse\u003C\u002Fh2>\n\u003Cp>Here's the part I got burned by. Once \u003Ccode>$fillable\u003C\u002Fcode> is defined, keys that aren't on the list don't throw. They get dropped. Quietly.\u003C\u002Fp>\n\u003Cp>My \u003Ccode>role\u003C\u002Fcode> field never saved on that first deploy because I had forgotten to add it to the list, and nothing told me. The page reloaded fine, the row existed, the column was just empty. In development that's the nastiest possible failure mode, because everything looks like it worked.\u003C\u002Fp>\n\u003Cp>One line in \u003Ccode>AppServiceProvider\u003C\u002Fcode> changes that:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-php\">use Illuminate\\Database\\Eloquent\\Model;\n\npublic function boot(): void\n{\n    Model::preventSilentlyDiscardingAttributes($this-&gt;app-&gt;isLocal());\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Now any attempt to fill a discarded attribute throws in local environments. Production keeps the forgiving behavior, which you may actually want for fields that vary between form versions. I run this on every project now. It caught two more forgotten keys the same week.\u003C\u002Fp>\n\n\u003Ch2>$guarded = [] is the opposite of a fix\u003C\u002Fh2>\n\u003Cp>Search \"mass assignment\" and you'll find answers suggesting \u003Ccode>$guarded = []\u003C\u002Fcode>. That empties the block list and makes every column writable, including \u003Ccode>id\u003C\u002Fcode>, \u003Ccode>role\u003C\u002Fcode>, \u003Ccode>is_admin\u003C\u002Fcode>, and whatever column someone adds next year. The exception goes away. So does the thing that caused it.\u003C\u002Fp>\n\u003Cp>A block list only works if you remember every field to block, forever. An allow list just asks the new column to earn its place in a review. That's the whole reason \u003Ccode>$fillable\u003C\u002Fcode> ages better, and it's why my inherit-three-years-later moment happened at all.\u003C\u002Fp>\n\u003Cp>If you want a quick audit, grep your codebase for \u003Ccode>guarded = []\u003C\u002Fcode> and for \u003Ccode>forceFill()\u003C\u002Fcode>. The first is almost always wrong on models that touch requests. The second bypasses the protection on purpose, which is fine in a seeder and worth a raised eyebrow in a controller. On a related note, when you're auditing a model, check its query count too. I wrote about that in \u003Ca href=\"\u002Fposts\u002Flaravel-n-plus-one-queries-find-fix\u002F\">finding and fixing N+1 queries\u003C\u002Fa>.\u003C\u002Fp>\n\n\u003Ch2>Laravel 13 makes it an attribute\u003C\u002Fh2>\n\u003Cp>Since Laravel 13 you can also declare this on the class itself:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-php\">#[Fillable(['name', 'email', 'password'])]\nclass User extends Model\n{\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>It's optional and backward compatible, so the property version isn't going anywhere. I like the attribute for small models because the list sits right at the top where the eye lands. On models with fifteen fillable fields I still use the property, mostly because a long attribute list reads badly.\u003C\u002Fp>\n\n\u003Ch2>What I do now\u003C\u002Fh2>\n\u003Cp>Form request at the boundary, \u003Ccode>validated()\u003C\u002Fcode> everywhere I would have written \u003Ccode>all()\u003C\u002Fcode>, and \u003Ccode>$fillable\u003C\u002Fcode> as the second line of defense rather than the only one. The fillable-plus-validated controller pattern is one of the snippets I keep in \u003Ca href=\"\u002Fsnippetark\u002F\" rel=\"noopener noreferrer\" target=\"_blank\">Snippet Ark\u003C\u002Fa> so new controllers start from it instead of from memory. Do that and the exception shows up once, in development, on the day you add a column. Which is exactly when you want to see it.\u003C\u002Fp>\n","2026-10-24",1793106023371]