A list of fifty invoices touches $invoice->lines in the view. That is fifty extra queries, and none of them show up in review. The page is just slower. Lazy loading hides the query until the relationship is touched.

preventLazyLoading() throws as soon as that happens, outside production. The exception names the model and the relation, so the fix is usually one eager load. Leave the guard off in production so a missed relation degrades instead of taking the request down.

01 What it does

The guard watches every lazy load. The page that always shows lines says so on the query. Nested relations use the dot.

InvoiceController.phpPHP
// app/Http/Controllers/InvoiceController.php
use App\Models\Invoice;

$invoices = Invoice::query()
    ->with(['team', 'lines'])
    ->latest()
    ->paginate(20);

with() runs those queries up front: one for teams, one for lines. The loop then reads memory.

A route model binding has already loaded the invoice. loadMissing() fetches a relation only if it is not loaded yet.

InvoiceController.phpPHP
// app/Http/Controllers/InvoiceController.php
use App\Models\Invoice;
use Illuminate\View\View;

class InvoiceController
{
    public function show(Invoice $invoice): View
    {
        $invoice->loadMissing(['team', 'lines']);

        return view('invoices.show', ['invoice' => $invoice]);
    }
}

With the guard on during testing, rendering the page is enough. If the view touches a relation you forgot, the test throws before the assertion.

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

it('does not lazy load the invoice index', function () {
    Invoice::factory()->count(10)->create();

    $this->get(route('invoices.index'))->assertOk();
});

02 Gotchas

  • The guard is off when app()->isProduction() is true. A missed relation in production still lazy loads. The test is where it has to fail.
  • loadMissing() does not help a collection you already looped. Call it before the view, or use with() on the query.
  • A relation touched inside an accessor still counts. The exception names that accessor’s model.
  • shouldBeStrict() turns this on together with the discard guard and the missing-attribute guard. Refuse attributes a model will throw away is the sibling.

Eager loading on purpose is the lesson. Catching N+1 queries is the same guard in the series. Listen to one request’s queries is how you see the queries before the guard is on.