skip to content

Relationship Types

Declaring relations as methods: hasMany and belongsTo, many-to-many through a pivot table, has-many-through, one-of-many and polymorphic morphs. Interviewers ask which side holds the key and why.

on this pageshow

explore

questions

5

In Eloquent, how do hasOne, hasMany and belongsTo differ, and which model's table holds the foreign key in each?

level: juniorimportance: must knowfreq 80%

answer

  1. the side holding the column says belongsTo
  2. hasMany key from the parent class name
  3. belongsTo key from the method name
  4. course_id on lessons
  5. hasOne returns a model or null

basics

~20 s

hasOne and hasMany sit on the parent and expect the foreign key on the related table; belongsTo sits on the model whose own table holds that key. hasMany returns a Collection, hasOne and belongsTo a single model or null.

solid answer

~40 s

All three are methods on the model that return relation objects. `hasOne(StudentCard::class)` on `Student` and `hasMany(Lesson::class)` on `Course` expect the foreign key on the **related** table, `student_cards.student_id` and `lessons.course_id`, named from the parent class. `belongsTo(Course::class)` on `Lesson` is the inverse: the key is on the lesson's **own** table, and Eloquent guesses its name from the relationship method, so a method called `course()` looks for `course_id`. `hasMany` returns a Collection; `hasOne` and `belongsTo` return one model or `null`, and `withDefault()` can replace that null with an empty model. When columns do not follow the convention, pass the foreign key and the local or owner key as extra arguments.

go deeper

for a junior

Recall that the model whose table holds the key column uses belongsTo, and the model it points at uses hasOne or hasMany.

for a middle

Explain how Eloquent guesses the key names on each side, why belongsTo depends on the method name, and how to pass keys explicitly.

for a senior

Review relation declarations for mismatched keys and types, choose withDefault deliberately, and keep both ends of a link consistent.

for a principal

Decide how strictly the team follows naming conventions versus explicit key arguments, given legacy schemas and future renames.

## Relations are methods In Eloquent a **relationship** is a public method on the model that returns a relation object, for example `HasMany` or `BelongsTo`. The method both describes the link and serves as a query builder for the related rows. The three basic types differ in one thing above all: **which table holds the foreign key**. | Declared on | Call | Foreign key lives on | Returns | |---|---|---|---| | `Student` | `$this->hasOne(StudentCard::class)` | `student_cards.student_id` | one model or `null` | | `Course` | `$this->hasMany(Lesson::class)` | `lessons.course_id` | a Collection | | `Lesson` | `$this->belongsTo(Course::class)` | `lessons.course_id` (its own table) | one model or `null` | Rule of thumb: the model whose table **carries** the key column says `belongsTo`; the model the key **points at** says `hasOne` or `hasMany`. ## How Eloquent guesses the keys The guesses come from different places for the two sides, which is where bugs hide. - **`hasOne` / `hasMany`** take the foreign-key name from the **parent class**: `getForeignKey()` returns the snake_case class basename plus `_` plus its key name, so `Course` gives `course_id`. The local key defaults to the parent's primary key. - **`belongsTo`** takes the foreign-key name from the **relationship method's name**: snake_case of the method plus `_` plus the related model's key name. A method named `course()` looks for `course_id`. - The owner key on `belongsTo` defaults to the related model's primary key. That second rule produces a classic surprise. A `Lesson` method named `module()` that returns `$this->belongsTo(Course::class)` looks for a `module_id` column, not `course_id`. Either name the method after the column or pass the key explicitly. ## Overriding the conventions Each method accepts the key names as extra arguments: ```php class Course extends Model { public function lessons(): HasMany { return $this->hasMany(Lesson::class, 'course_ref', 'code'); } } class Lesson extends Model { public function course(): BelongsTo { return $this->belongsTo(Course::class, 'course_ref', 'code'); } } ``` 1. `hasMany($related, $foreignKey, $localKey)` and `hasOne(...)` name the column on the **child** table, then the column on the parent it matches. 2. `belongsTo($related, $foreignKey, $ownerKey)` names the column on **this** table, then the column on the parent it points to. The two sides of one link must name the same pair of columns, just from opposite ends. ## Reading the result Accessed as a property, `$course->lessons` returns a Collection (empty when there are none), while `$lesson->course` and `$student->card` return a model or `null`. Calling the method, `$course->lessons()`, returns the relation object so you can add constraints before fetching. ## Handling the missing side A `belongsTo` or `hasOne` that finds nothing yields `null`, which breaks templates that chain on it. `withDefault()` returns an empty model instead, optionally pre-filled: ```php public function course(): BelongsTo { return $this->belongsTo(Course::class)->withDefault(['title' => 'Unassigned']); } ``` ## Common mistakes - Declaring `hasMany` on the model whose table holds the key; the relation then queries the wrong table. - Forgetting that `belongsTo`'s guess depends on the method name, not the class name. - Mismatched key types between the two columns, for example a string key on one side and an integer on the other. - Returning `hasOne` where the data allows many rows; Eloquent returns one of them without complaint. ## A worked check Given `students`, `student_cards` and `courses`/`lessons`, walk each declaration and ask which column it reads: 1. `Student::card()` returns `$this->hasOne(StudentCard::class)`: parent class `Student` gives `student_id`, looked up on `student_cards`. 2. `StudentCard::student()` returns `$this->belongsTo(Student::class)`: method name `student` gives `student_id`, read from the card's own row. 3. `Course::lessons()` returns `$this->hasMany(Lesson::class)`: `course_id` on `lessons`. 4. `Lesson::course()` returns `$this->belongsTo(Course::class)`: `course_id` on the lesson itself. Each pair names the same column from opposite ends, which is the property to verify in review. Declaring the return types, `HasOne`, `HasMany` and `BelongsTo`, also documents the direction for the next reader and lets static analysis catch a method that returns the wrong kind of relation.

  • On a `Lesson` model, what column does `public function module(): BelongsTo { return $this->belongsTo(Course::class); }` look for?
    `module_id`. `belongsTo()` builds the foreign-key name from the relationship method's name in snake_case plus the related key name, not from the related class. Rename the method to `course()` or pass `'course_id'` as the second argument.
  • What does `withDefault()` change on a `belongsTo` relation?
    When no related row exists, the relation returns a new, empty instance of the related model instead of `null`, optionally filled from an array or a closure. Templates and code that chain on the relation no longer need a null check, but the default model is not saved and has no key.

saying these in an interview costs you the question

  • belongsTo means the foreign key is on the other model's table
  • hasMany derives the foreign key from the relationship method name
  • hasOne and hasMany need a pivot table
  • Accessing a hasMany relation with no rows returns null
  • Both sides of a one-to-many relation declare hasMany
open as a page

In Eloquent, how do you declare a many-to-many relation whose pivot table stores a grade, and when do you add a custom pivot with using()?

level: middleimportance: must knowfreq 70%

basics

~20 s

Declare belongsToMany() on both models, backed by an alphabetically named pivot table such as course_student, and add withPivot('grade') to read the extra column. Add using() with a Pivot subclass when the pivot row needs casts or behaviour.

open as a page

In Eloquent, what do hasOneThrough() and hasManyThrough() declare, and which table does each of their key arguments refer to?

level: middleimportance: should knowfreq 40%

basics

~20 s

hasManyThrough() and hasOneThrough() reach models two has-type hops away through an intermediate model in one query. After the final and intermediate classes, the third argument is the key on the intermediate table and the fourth the key on the final table.

open as a page

In Eloquent, how do morphTo, morphMany and morphToMany model polymorphic relations, and why call Relation::enforceMorphMap() in production apps?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Polymorphic relations store a type and an id on the child so one model can belong to several parent types. enforceMorphMap() stores short aliases instead of class names and throws for unmapped models, so renames and new models cannot corrupt the type column.

open as a page

In Eloquent, what do latestOfMany() and ofMany() add to a hasOne relation, and why prefer them over hasMany()->latest()->first()?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

latestOfMany(), oldestOfMany() and ofMany() turn a hasOne into the single latest, oldest or best row of a has-many, resolved by a grouped subquery join. Unlike latest()->first(), it is a real relation that can be eager loaded for many parents at once.

open as a page