Database tables are often related to one another — a user has many posts, a post belongs to a user, users have many roles through a pivot table. Eloquent relationships let you define these connections as methods on your models, and FoxDB handles the underlying queries and joins.
FoxDB supports five relationship types: HasOne, HasMany, BelongsTo, BelongsToMany, and HasManyThrough. Each is both lazy-loadable (loaded on first access) and eager-loadable (loaded up front to avoid the N+1 query problem).
A User has one Profile. The foreign key (user_id) lives on the profiles table.
class User extends Model
{
public function profile(): HasOne
{
return $this->hasOne(Profile::class, 'user_id', 'id');
// hasOne(related, foreignKey, localKey)
}
}$profile = $user->profile; // Profile|nullThe inverse: a Profile belongs to a User.
class Profile extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id', 'id');
// belongsTo(related, foreignKey on this table, ownerKey on related table)
}
}$user = $profile->user; // User|null// Set the foreign key by passing the related model
$profile->user()->associate($user);
$profile->save();
// Clear the foreign key
$profile->user()->dissociate();
$profile->save();A User has many Post records. The foreign key (user_id) lives on the posts table.
class User extends Model
{
public function posts(): HasMany
{
return $this->hasMany(Post::class, 'user_id', 'id');
// hasMany(related, foreignKey, localKey)
}
}
class Post extends Model
{
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
}$posts = $user->posts; // Collection<Post>
$author = $post->author; // User|nullAll four key arguments to hasOne, hasMany, and belongsTo are optional — FoxDB derives sensible defaults from the model names (e.g. user_id for a User relation, and the related model's primary key). Pass them explicitly whenever your schema doesn't follow the convention.
Users can have many roles, and roles can belong to many users, through a pivot table (user_role by default — alphabetical snake_case of both model names).
class User extends Model
{
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, 'user_role', 'user_id', 'role_id');
// belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey)
}
}$roles = $user->roles; // Collection<Role>// Attach
$user->roles()->attach(3);
$user->roles()->attach([3, 5, 7]);
$user->roles()->attach(3, ['granted_at' => date('Y-m-d')]); // with pivot data
// Detach
$user->roles()->detach(3);
$user->roles()->detach(); // detach all
// Sync — attach the given IDs, detach everything else
$user->roles()->sync([3, 5]);
// Toggle — attach if missing, detach if present
$user->roles()->toggle([3, 5]);
// Check
$user->roles()->isAttached(3); // bool
// Update pivot row data without detaching
$user->roles()->updateExistingPivot(3, ['granted_at' => date('Y-m-d')]);$roles = $user->roles()->withPivot('granted_at', 'expires_at')->get();
foreach ($roles as $role) {
echo $role->pivot->granted_at;
}HasManyThrough lets you access a relation through an intermediate model. For example, to get all comments on a user's posts without loading the posts themselves:
class User extends Model
{
public function comments(): HasManyThrough
{
return $this->hasManyThrough(
Comment::class, // final related model
Post::class, // intermediate model
'user_id', // FK on posts referencing users
'post_id', // FK on comments referencing posts
'id', // local key on users
'id', // local key on posts
);
}
}$comments = $user->comments; // Collection<Comment>By default, relations are loaded the first time you access them, and the result is cached on the model instance for subsequent access.
$user = User::find(1);
$posts = $user->posts; // runs a query
$posts = $user->posts; // returns the cached result — no second queryLazy loading a relation inside a loop causes the N+1 problem: one query for the parent rows, plus one additional query per row for the relation.
// 1 + N queries
$users = User::all();
foreach ($users as $user) {
echo $user->posts->count(); // a separate query for every user
}with() solves this by loading the relation for all parent models in a single additional query:
// Exactly 2 queries total
$users = User::with('posts')->get();
foreach ($users as $user) {
echo $user->posts->count(); // already loaded
}$users = User::with('posts', 'profile', 'roles')->get();Pass a closure to filter or order the eager-loaded relation:
$users = User::with([
'posts' => fn($query) => $query->where('published', 1)->orderBy('created_at', 'desc')
])->get();Eager-loaded relations are included automatically in toArray():
$data = User::with('posts')->get()->toArray();
// $data[0]['posts'] is an array of post arrayswith() is order-independent — it can appear as the first call or anywhere after select(), where(), or any other query method, and the final result is the same:
// All equivalent:
User::with('posts')->select('id', 'name')->where('active', 1)->get();
User::select('id', 'name')->with('posts')->where('active', 1)->get();
User::where('active', 1)->select('id', 'name')->with('posts')->get();Version note: this order-independence relies on
select()(and other forwarded query methods) returning a model-aware builder. On FoxDB versions whereModel::select(...)returns a relation-unawareFoxdb\Query\Builderinstead, chaining->with(...)afterselect(...)raisesCall to unknown method: Foxdb\Query\Builder::with(). If you hit that error, either putwith()first in the chain, or update FoxDB.
Use lazy loading when you only access the relation for a single model — for example, on a "show" page for one record. Use eager loading (with()) whenever you're iterating over a list of models and will access a relation on each one.