mpyw/eloquent-has-by-non-dependent-subquery
Installation:
composer require mpyw/eloquent-has-by-non-dependent-subquery
No additional configuration is required—just use it with Eloquent queries.
First Use Case:
Replace a slow has() or whereHas() query with a non-dependent subquery for MySQL optimization:
// Before (dependent subquery)
User::whereHas('posts', function ($query) {
$query->where('title', 'like', '%test%');
})->get();
// After (non-dependent subquery, optimized)
User::hasByNonDependentSubquery('posts', function ($query) {
$query->where('title', 'like', '%test%');
})->get();
Where to Look First:
hasByNonDependentSubquery() or whereHasByNonDependentSubquery() as drop-in replacements for Eloquent’s built-in methods.Replacing has():
// Original
User::has('posts')->get();
// Optimized
User::hasByNonDependentSubquery('posts')->get();
Replacing whereHas():
// Original
User::whereHas('posts', fn($q) => $q->where('published', true))->get();
// Optimized
User::whereHasByNonDependentSubquery('posts', fn($q) => $q->where('published', true))->get();
Combining with Other Constraints:
User::where('active', true)
->whereHasByNonDependentSubquery('posts', fn($q) => $q->where('views', '>', 100))
->orderBy('created_at', 'desc')
->get();
Dynamic Relationships:
$relation = 'posts';
User::whereHasByNonDependentSubquery($relation, fn($q) => $q->where('title', 'like', '%'.$search.'%'))->get();
Use in Repositories:
Replace has()/whereHas() calls in repository methods with the optimized versions.
public function findActiveUsersWithPublishedPosts()
{
return User::where('active', true)
->whereHasByNonDependentSubquery('posts', fn($q) => $q->where('published', true))
->get();
}
API Controllers: Apply optimizations in query scopes or controller logic:
public function index(Request $request)
{
$query = User::query();
if ($request->has('search')) {
$query->whereHasByNonDependentSubquery('posts', fn($q) => $q->where('title', 'like', '%'.$request->search.'%'));
}
return $query->get();
}
Service Layer: Encapsulate optimized queries in services for reusability:
class UserService {
public function getUsersWithRecentPosts(int $days = 7)
{
return User::whereHasByNonDependentSubquery('posts', fn($q) =>
$q->where('created_at', '>', now()->subDays($days))
)->get();
}
}
MySQL-Specific Optimization:
Query Builder Limitations:
whereHas() calls may not work as expected. Flatten logic where possible.whereHasByNonDependentSubquery() might not convert correctly.Debugging:
toSql() and toSql() with bindings() to verify generated SQL:
$query = User::whereHasByNonDependentSubquery('posts', fn($q) => $q->where('title', 'like', '%test%'));
dd($query->toSql(), $query->getBindings());
has()/whereHas() to ensure correctness.Performance Caveats:
EXPLAIN to confirm improvements. Some queries may perform worse due to subquery overhead.posts) have proper indexes on filtered columns (e.g., title, published).Partial Replacement:
DB::enableQueryLog() to identify bottlenecks:
DB::enableQueryLog();
User::whereHas('posts', fn($q) => $q->where('title', 'like', '%test%'))->get();
dd(DB::getQueryLog());
Fallback Logic:
if (app()->environment('production') && !config('database.connections.mysql.version_8_plus')) {
User::whereHasByNonDependentSubquery(...);
} else {
User::whereHas(...);
}
Testing:
has() and hasByNonDependentSubquery():
public function test_has_by_non_dependent_subquery()
{
$usersWithPosts = User::factory()->has('posts')->count();
$optimizedCount = User::hasByNonDependentSubquery('posts')->count();
$this->assertEquals($usersWithPosts, $optimizedCount);
}
Extension Points:
use Illuminate\Database\Eloquent\Builder;
Builder::macro('hasOptimized', function ($relation, $callback = null, $operator = '>=', $count = 1) {
return $this->whereHasByNonDependentSubquery($relation, $callback, $operator, $count);
});
Monitoring:
How can I help you explore Laravel packages today?