Installation:
composer require mpyw/eloquent-has-by-join
No additional configuration is required—just use the package in your Eloquent models.
First Use Case:
Replace has() or whereHas() with hasByJoin() for single-result relations (e.g., belongsTo, hasOne).
Example:
// Before (subquery)
Post::whereHas('comments', fn($q) => $q->where('active', true));
// After (join)
Post::hasByJoin('comments', fn($q) => $q->where('active', true));
Where to Look First:
Replace has()/whereHas():
Use hasByJoin() for relations where you expect a single result (e.g., belongsTo, hasOne).
// Original
User::whereHas('profile', fn($q) => $q->where('verified', true));
// Optimized
User::hasByJoin('profile', fn($q) => $q->where('verified', true));
Chaining with Other Constraints:
Combine with join(), where(), or with() seamlessly:
Post::hasByJoin('author', fn($q) => $q->where('active', true))
->join('tags', 'posts.id', 'tags.post_id')
->where('published', true);
Customizing Join Conditions: Override default join logic via the closure:
Post::hasByJoin('comments', fn($q) => $q
->where('active', true)
->select('post_id', 'id') // Explicit columns for join
);
Soft Deletes:
Works out-of-the-box with SoftDeletes:
Post::hasByJoin('comments')->withTrashed();
hasByJoin (e.g., hasByJoin('user.posts')). Flatten logic:
// Instead of:
User::hasByJoin('posts.comments', ...);
// Use:
User::hasByJoin('posts', fn($q) => $q->hasByJoin('comments', ...));
MySQL Version:
Non-Single-Result Relations:
hasMany, belongsToMany, or polymorphic relations. Use has() instead.// ❌ Fails silently (returns all matching rows)
Post::hasByJoin('comments'); // Comments is hasMany
Column Selection:
select(): May cause Cartesian products if joined tables lack constraints.Post::hasByJoin('author', fn($q) => $q->select('id', 'user_id'));
Debugging:
DB::enableQueryLog() to compare has() vs. hasByJoin() output.where clauses in the closure can lead to unintended joins.Hybrid Approach:
Combine with orHasByJoin() for complex conditions:
Post::whereHas('comments')
->orHasByJoin('replies', fn($q) => $q->where('urgent', true));
Custom Macros:
Extend the package by adding macros to your Model:
use Illuminate\Database\Eloquent\Builder;
Builder::macro('hasActiveByJoin', function($relation) {
return $this->hasByJoin($relation, fn($q) => $q->where('active', true));
});
Testing:
hasByJoin behavior:
$builder = $this->partialMock(Builder::class, [], function($mock) {
$mock->shouldReceive('getQuery')->andReturnSelf();
});
Fallback Logic:
Use a helper to auto-switch between has() and hasByJoin() based on DB:
function hasRelation($relation, $callback = null, $useJoin = true) {
$db = DB::connection()->getDatabaseName();
return $useJoin && str_starts_with($db, 'mysql') && version_compare(DB::version(), '8.0.16', '<')
? $this->hasByJoin($relation, $callback)
: $this->has($relation, $callback);
}
Indexing: Ensure foreign keys are indexed for optimal join performance:
Schema::table('comments', function (Blueprint $table) {
$table->foreignId('post_id')->constrained()->index();
});
How can I help you explore Laravel packages today?