The eager load of team and lines only helps if a missing one fails where you can see it. Outside production, turn lazy loading into an exception. A page that quietly runs one query per invoice is the bug. The exception names it before a customer has to.
Eloquent, properly · Lesson 3
Catching N+1 queries
Video
01 Fail the test, not the customer
preventLazyLoading() belongs in a service provider, switched off in production so a missed load degrades to a query instead of a 500. The flag is the inverse of isProduction(), which keeps local, testing and staging strict. Production stays lenient on purpose.
class Example
{
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}
}A feature test that renders the invoice list will now throw if the controller forgot with('lines'). That is the bug you wanted. The exception is LazyLoadingViolationException, and the message names the model and the relation. Add that name to the with() list for the action the test hit. Do not remove a line from the view to make the query count look small if the view still needs the line.
The short version, with the same guard, is the snippet this series keeps pointing at.
Catch N+1 queries before your users do.
When the exception fires, add the relationship to the with() list for that page. Do not disable the guard to make the test green. The guard is the point. A single test that calls preventLazyLoading(false) teaches the next person that the exception is optional. Leave the boot method as the only switch.
02 Let the feature test see the exception
Laravel's HTTP kernel turns an exception into an error response. A test that calls get() and assertOk() fails with a 500, which is a start, but the failure does not say "lines". Call withoutExceptionHandling() and the same request fails as LazyLoadingViolationException. That is the message you can read, and the one a pull request review can recognise.
it('renders the invoice list without a lazy load', function () {
$this->withoutExceptionHandling();
$team = Team::factory()->create();
$invoice = $team->invoices()->create([
'number' => 'INV-1',
'issued_on' => '2026-10-01',
'status' => 'issued',
]);
$invoice->lines()->create([
'description' => 'Hosting',
'amount' => 2400,
]);
$this->actingAs($invoice->team->owner)
->get('/invoices')
->assertOk();
});Create the line, and sign in as $invoice->team->owner. A user from User::factory()->create() is not on that team, so the list is empty and the guard never runs. The view has to read lines, or the test has to, before the exception exists. An index that is green and a show page that still lazy-loads is the same rule: the guard fires on the code path that touches the relation. Render the show page in its own test if the show view reads something the index does not.
automaticallyEagerLoadRelationships() will fetch a relation the moment the view touches it, which hides the N+1 and also loads whatever the template happens to mention. It is not a substitute for the guard, and it is not the with() list this series asked you to write. Leave it off. The explicit list stays the place you decide what this response costs.
Production still degrades. A missed relation there is extra queries, not a 500, because the guard is off. The test is what stops that miss shipping. If a staging box should behave like production, point isProduction() at that environment too, and keep the test suite on an environment where the guard is on.
Recap
You can make a forgotten eager load throw outside production, and see that exception in the feature test that renders the page.
preventLazyLoading()names the model and the relation.- A feature test that renders the list is enough to catch it, once the list has lines to touch.
- Turning the guard off to make the test green removes the point of the guard.
withoutExceptionHandling()lets the test fail withLazyLoadingViolationExceptioninstead of a bare 500.
Check yourself · 3 questions
Question 1 of 3The invoice index throws LazyLoadingViolationException for lines. What do you change?