When you need to join tables based on Eloquent relationships, skip the manual join() calls and use the power-join package’s joinRelationship() method instead.
The Old Way: Manual Joins
use Illuminate\Support\Facades\DB;
$report = Report::select('reports.*')
->join('external_users', fn ($join) => $join
->on(DB::raw('BINARY reports.email'), DB::raw('BINARY external_users.user_email'))
->where('external_users.id', config('services.external.user_id'))
)
->firstOrFail();
This works, but it’s verbose and duplicates your relationship logic. You’re manually specifying the join conditions that are already defined in your Eloquent relationships.
The Better Way: joinRelationship()
$report = Report::joinRelationship('externalUser', fn($q) =>
$q->whereKey(config('services.external.user_id'))
)->firstOrFail();
Much cleaner! The joinRelationship() method:
- Uses your existing
Report::externalUser()relationship definition - Generates the join automatically based on the relationship type
- Lets you add WHERE conditions via the closure
- Keeps your query logic DRY
Setup
Install the package:
composer require kirschbaum-development/eloquent-power-joins
Define your relationship once in the model:
// app/Models/Report.php
class Report extends Model
{
public function externalUser(): HasOne
{
return $this->hasOne(ExternalUser::class,
foreignKey: 'user_email',
localKey: 'email'
);
}
}
Now any query that needs this join can use joinRelationship('externalUser') instead of repeating the join logic.
Why This Matters
When your join logic lives in one place (the relationship method), changes only need to happen once. If you later add a scope, change the foreign key, or adjust the join conditions, all your queries automatically pick up the change.
Bonus: joinRelationship() also works with nested relationships like joinRelationship('company.department'), giving you powerful query composition without the complexity.