Laravel Eloquent query optimization is difficult at runtime. Learn how to use Abstract Syntax Tree (AST) analysis to detect inefficient relationship loading.
Last month, our team spent about two days tracking down a memory leak in a dashboard report service that only triggered under a specific set of filter permutations. It turned out to be a classic N+1 query issue hidden inside a nested loop, which bypassed our standard integration tests because the test dataset was too small to trigger the OOM killer.
We realized then that relying on runtime debugging or simple logging isn't enough when you're managing complex domain models. To solve this, we moved toward Laravel Eloquent query optimization using static analysis and Abstract Syntax Tree (AST) parsing.
We first tried adding a custom QueryExecuted listener to log duplicate queries in development. It worked for simple cases, but it failed to catch issues in conditional logic or background jobs that weren't being hit during manual testing. We even looked into Laravel Proxy Pattern for Eloquent Lazy Loading Optimization to mitigate the damage, but that's a reactive fix.
Static analysis allows us to inspect the code structure itself. By parsing the PHP source code into an AST, we can detect calls to Eloquent relationships within loops or iterators without ever executing a single line of the suspect code.
We started by using the nikic/php-parser library, which is the industry standard for parsing PHP code into an AST. The goal was to write a visitor that identifies property access on Eloquent models inside foreach or while blocks.
Here is a simplified version of what our visitor looks like:
PHPuse PhpParser\Node; use PhpParser\NodeVisitorAbstract; class EloquentRelationshipVisitor extends NodeVisitorAbstract { public function enterNode(Node $node) { #6A9955">// Detect loops if ($node instanceof Node\Stmt\Foreach_) { $this->isInsideLoop = true; } #6A9955">// Detect property access(e.g., $user->posts) if ($this->isInsideLoop && $node instanceof Node\Expr\PropertyFetch) { $this->reportPotentialNPlusOne($node); } } }
This approach is powerful because it's deterministic. Unlike runtime tests, which depend on your input data, the AST analysis covers 100% of your code paths.
To make this practical for a team, we integrated this check into our CI pipeline using a custom PHPStan rule. PHPStan already builds an AST, so we don't have to manage the parser ourselves.
When we detect an Eloquent relationship being accessed inside a loop, we flag it as an error. We also cross-reference the class against our Mastering Laravel Eloquent Scopes: Writing Reusable Query Constraints to ensure that if a developer is fetching data, they are at least using the correct scopes.
However, static analysis has limitations. It can’t always tell if a collection has already been eager-loaded via ->load() earlier in the controller method. We’ve found that we have to pair this with Laravel Eloquent Hydration Optimization: Reducing Reflection Overhead to ensure the overhead of these checks doesn't slow down our build times too significantly.
The biggest hurdle wasn't the AST logic; it was the "noise." Initially, our tool flagged thousands of false positives because it couldn't distinguish between a standard property and an Eloquent relationship.
We had to implement a whitelist strategy:
Illuminate\Database\Eloquent\Model.We’re still refining the analyzer. Sometimes, a loop should lazy-load a relationship if the logic is rare or memory-constrained. We’re currently experimenting with docblock annotations like /** @no-n-plus-one-check */ to suppress warnings where the developer has made an intentional architectural choice.
If you’re struggling with similar performance issues, stop relying on your eyes to catch them in code review. Implementing static analysis to enforce Eloquent performance standards might seem like overkill for a small app, but for a high-scale Laravel system, it’s the only way to sleep through the night.
Does AST analysis slow down CI builds? It adds roughly 10-15 seconds to our total build time. We run the analysis only on changed files during pre-commit hooks to keep the developer experience fast.
Can this detect N+1 issues in closures?
Yes, but it's complex. You have to track the scope of the variable through the AST nodes. We currently only support simple foreach loops to keep the complexity manageable.
Is this better than using a package like laravel-query-detector?
They serve different purposes. laravel-query-detector is great for local development and catching issues at runtime. AST analysis is for prevention—it stops the code from ever reaching the repository if it violates your performance constraints.
I’m still not 100% satisfied with our current false-positive rate on polymorphic relationships. It’s a work in progress, but the peace of mind is worth the effort.
Laravel Eloquent withCount helps you avoid N+1 issues when counting related models. Learn how to use it to boost your database performance today.