## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require staudenmeir/belongs-to-through
Verify compatibility with your Laravel version (e.g., ^2.18 for Laravel 13.x).
First Use Case:
Define a BelongsToThrough relationship in a model where a parent model indirectly owns a child model via one or more intermediate models.
Example: A User belongs to a Team through UserTeam and TeamMembership:
// User.php
use Staudenmeir\LaravelBelongsToThrough\HasBelongsToThrough;
class User extends Model
{
use HasBelongsToThrough;
public function team()
{
return $this->belongsToThrough(
Team::class, // Target model
UserTeam::class, // First intermediate model
'user_id', // Foreign key on first intermediate
'team_id', // Foreign key on first intermediate
'id', // Owner key on first intermediate
TeamMembership::class, // Second intermediate model
'user_team_id', // Foreign key on second intermediate
'team_id', // Foreign key on second intermediate
'id' // Owner key on second intermediate
);
}
}
First Query:
$user = User::find(1);
$team = $user->team; // Eagerly loads the team via intermediates
Basic Relationships:
belongsToThrough() for single-step or multi-step indirect relationships.return $this->belongsToThrough(
Target::class,
Intermediate1::class, 'fk1', 'fk2', 'owner_key1',
Intermediate2::class, 'fk3', 'fk4', 'owner_key2',
Intermediate3::class, 'fk5', 'fk6', 'owner_key3'
);
Eager Loading:
with() to optimize queries:
$users = User::with('team')->get();
$users = User::with('team.members')->get();
Custom Constraints:
where() clauses to filter intermediate models:
$user->team()->where('active', true);
whereHas() for deeper constraints:
$user->team()->whereHas('memberships', fn($q) => $q->where('role', 'admin'));
Polymorphic Relationships:
return $this->belongsToThrough(
Target::class,
Intermediate::class,
'owner_id', 'owner_type', 'intermediate_id'
);
Dynamic Relationships:
when() to conditionally define relationships:
public function optionalTeam()
{
return $this->when($this->isPremium(), function() {
return $this->belongsToThrough(Team::class, UserTeam::class, ...);
});
}
public function toArray($request)
{
return [
'team' => TeamResource::make($this->whenLoaded('team')),
];
}
public function rules()
{
return [
'team_id' => 'required|exists:teams,id',
'membership_data' => 'sometimes|array',
];
}
UserTeam::observe(UserTeamObserver::class);
// UserTeamObserver.php
public function saved(UserTeam $userTeam)
{
$userTeam->user->team->refresh();
}
Key Mismatches:
dd($this->team->getQuery()->toSql()) to verify generated queries.Missing Intermediate Models:
null.exists() checks:
if ($user->team()->exists()) { ... }
N+1 Queries:
// Bad: N+1 queries
foreach ($users as $user) {
$user->team; // Triggers a query per user
}
// Good: Single query
$users->load('team');
Circular References:
User->Team->User).withDefault() to break cycles:
return $this->belongsToThrough(...)->withDefault();
Soft Deletes:
SoftDeletes for proper soft-delete handling:
use Illuminate\Database\Eloquent\SoftDeletes;
class UserTeam extends Model
{
use SoftDeletes;
}
DB::enableQueryLog();
$user->team;
dd(DB::getQueryLog());
toBase() to inspect the relationship structure:
dd($this->team->getRelationship()->toBase());
whereHas clauses, test intermediate queries separately:
$query = $this->team()->whereHas('memberships', fn($q) => $q->where('role', 'admin'));
dd($query->toSql());
public function getTeamNameAttribute()
{
return $this->team?->name ?? 'No Team';
}
BelongsToThrough::macro('withRole', function($role) {
return $this->whereHas('memberships', fn($q) => $q->where('role', $role));
});
// Usage:
$user->team->withRole('admin');
class TeamScope
{
public function scopeActive($query)
{
return $query->whereHas('memberships', fn($q) => $q->where('active', true));
}
}
// Usage:
$user->team()->active();
TeamMembership::observe(function($model) {
if ($model->wasRecentlyCreated) {
$model->user->team->refresh();
}
});
^2.18 for Laravel 13.x).$user->team->refresh();
DB::transaction(function() {
$userTeam = $user->userTeams()->create([...]);
$membership = $userTeam->memberships()->create([...]);
});
```markdown
### Pro Tips for Daily Use
1. **Partial Loading**:
Use `with()` to load only specific attributes of the target model:
```php
$user->load('team:id,name'); // Loads only id and name
Relationship Metadata:
Access metadata like exists or wasRecentlyCreated:
if ($user->team->exists) {
$user->team->touch(); // Update last accessed timestamp
}
Dynamic Intermediate Chains: Build relationships dynamically based on runtime conditions:
$intermediates = $this->getIntermediateModels();
$this->buildThroughRelationship($intermediates);
Performance Profiling:
Use Laravel Debugbar to compare query counts before/after implementing belongsToThrough.
Testing: Mock relationships in tests:
$user = User::factory()->create();
$team = Team::
How can I help you explore Laravel packages today?