If a column is missing from $fillable, Eloquent drops it on create() and says nothing. The request succeeds, the invoice is stored, and notes stays null. You notice when a customer asks where their note went.

This guard throws MassAssignmentException instead. The message names the attribute and the model: Add [notes] to fillable property to allow mass assignment on [App\Models\Invoice]. That is either a typo in the array you passed, or a column you forgot to opt in.

01 What it does

It sits in AppServiceProvider::boot(), next to preventLazyLoading(). Leave it off in production so an unexpected key degrades instead of failing the request.

AppServiceProvider.phpPHP
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;

class AppServiceProvider
{
    public function boot(): void
    {
        Model::preventSilentlyDiscardingAttributes(! app()->isProduction());
    }
}

shouldBeStrict() enables this guard, the lazy-loading guard, and the missing-attribute guard. One call covers the three ways Eloquent fails quietly.

02 Turn the strict checks on together

AppServiceProvider.phpPHP
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;

class AppServiceProvider
{
    public function boot(): void
    {
        Model::shouldBeStrict(! app()->isProduction());
    }
}

Use the single guard when you only want this check. Use shouldBeStrict() when you want all three and you have already fixed the noise they will surface.

Create through fillable attributes the way the controller does. A forgotten $fillable entry then fails the test instead of asserting a null you did not mean to allow.

InvoiceTest.phpPHP
// tests/Feature/InvoiceTest.php
use App\Models\Invoice;
use App\Models\Team;

it('stores notes when they are fillable', function () {
    $invoice = Invoice::factory()->for(Team::factory())->create([
        'notes' => 'Net 30',
    ]);

    expect($invoice->notes)->toBe('Net 30');
});

If notes is not fillable, this throws before the assertion. Adding it to $fillable makes it pass. Attributes you really do not want mass assigned, such as team_id set only by the relationship, stay off the list. Set those on the model after create(), or let the relationship set them.

03 Gotchas

  • The guard is off in production. A silent drop can still ship. The test is the place it has to throw.
  • $request->validated() often contains keys that are not columns, such as lines. Form requests uses safe()->except('lines') so this guard does not throw on a nested array.
  • forceFill() bypasses the guard. That is for the job that writes payment_attempt, not for request input.
  • A guarded attribute and an unguarded model ($guarded = []) are the opposite problem. This guard does not stop a model that accepts every column.

Form requests is the controller change this guard forces. Models that match the domain decides what is fillable. Catch N+1 queries before your users do is the lazy-loading half of shouldBeStrict().