In Eloquent, what do hasOneThrough() and hasManyThrough() declare, and which table does each of their key arguments refer to?
answer
- two hops through an intermediate model
- final model first, intermediate second
- third key on the intermediate table
- fourth key on the final table
- through('lessons')->has('submissions')
basics
~20 shasManyThrough() 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.
solid answer
~40 sThey declare a relation that crosses an intermediate model, such as `Course` to `Submission` via `Lesson`, and resolve it with a join in one query. The first two arguments are the final and intermediate classes. Then come four keys: the foreign key on the **intermediate** table (`lessons.course_id`), the foreign key on the **final** table (`submissions.lesson_id`), the local key on the declaring table and the local key on the intermediate table, all defaulting to the conventional names. `hasOneThrough()` is the single-result form. When both hops already exist as relations, `$this->through('lessons')->has('submissions')` composes them and reuses their keys. Both hops must be has-type links, not a pivot, and the relation is for reading and eager loading rather than creating rows.
go deeper
Recall that hasManyThrough reaches related rows through a model in the middle, such as a course's submissions via its lessons.
Explain the argument order, which table each key belongs to, and how the fluent through()->has() form reuses existing keys.
Choose between a through relation, nested eager loading and a query, depending on whether the intermediate data is needed and how it is written.
Weigh modelling deep paths as relations against explicit queries or read models when the domain has many multi-hop reports.
## What a through relation is Some links cross an intermediate model. A course has many lessons, and each lesson has many submissions; a teacher often wants "all submissions for this course" without looping over lessons. Eloquent's **through relations** declare that two-hop path once: - `hasManyThrough(Final::class, Intermediate::class)` returns **many** final models; - `hasOneThrough(Final::class, Intermediate::class)` returns **one** final model or `null`. ```php class Course extends Model { public function submissions(): HasManyThrough { return $this->hasManyThrough(Submission::class, Lesson::class); } } ``` The `submissions` table has no `course_id`. Eloquent resolves the relation in one query: 1. it selects from `submissions`; 2. it joins `lessons` on `lessons.id = submissions.lesson_id`; 3. it filters `lessons.course_id` to the course, or to every course when eager loading; 4. it adds `lessons.course_id` to the selected columns so eager-loaded results can be matched back to their course. ## The six arguments The key arguments are where people slip, because they alternate between the two tables: | Position | Name | Default for Course -> Lesson -> Submission | |---|---|---| | 1 | final model | `Submission::class` | | 2 | intermediate model | `Lesson::class` | | 3 | foreign key on the **intermediate** table | `course_id` (from the declaring model) | | 4 | foreign key on the **final** table | `lesson_id` (from the intermediate model) | | 5 | local key on the declaring table | `courses.id` | | 6 | local key on the intermediate table | `lessons.id` | So `hasManyThrough(Submission::class, Lesson::class, 'course_id', 'lesson_id', 'id', 'id')` spells out the defaults. ## The fluent form When both hops already exist as relations, `Course::lessons()` and `Lesson::submissions()`, you can compose them instead of repeating the keys: ```php public function submissions(): HasManyThrough { return $this->through('lessons')->has('submissions'); } ``` The dynamic spelling `$this->throughLessons()->hasSubmissions()` does the same. This form reuses the key names from the existing relations, so a custom key declared once is not restated, and it returns `hasManyThrough` when either hop is a has-many and `hasOneThrough` otherwise. ## `hasOneThrough` The same shape with single results: a student has one library card, and the card has one locker assignment, so `Student::hasOneThrough(LockerAssignment::class, LibraryCard::class)` returns the student's locker assignment or `null`. It is less common, but it answers "reach a single row two hops away" without loading the middle model. ## Limits - Both hops must be **has-type** links: the declaring model's key sits on the intermediate table, and the intermediate's key sits on the final table. A path that runs through a `belongsToMany` pivot is not a through relation. - The built-in through relations span exactly one intermediate model; longer chains need nested relations or a hand-written query. - The relation is **read-oriented**. You query and eager load it, but it does not fill in `lesson_id` for new rows, so submissions are created through `Lesson::submissions()`. ## Common mistakes - Swapping arguments 3 and 4, which makes the join compare the wrong columns and silently return nothing. - Declaring `hasManyThrough` when a real `belongsToMany` exists, or the reverse. - Using a through relation where the intermediate model's data is also needed; then load `lessons.submissions` instead so both levels are available. ## Through relation versus nested loading Two ways answer "a course's submissions", and they are not interchangeable: | | `$course->submissions` (through) | `$course->lessons` with each lesson's `submissions` | |---|---|---| | Result shape | one flat Collection of submissions | lessons, each holding its submissions | | Intermediate data | not returned | available | | Queries | one joined query | one for lessons, one for submissions when eager loaded | | Typical use | counts, lists, "all X for this Y" | pages grouped by the middle level | The through relation also works in the places any relation does: it can be eager loaded for many courses at once, constrained with extra `where` clauses on the final table, and counted. Soft-deleted intermediate rows are excluded when the intermediate model uses soft deletes, and `withTrashedParents()` includes them again.
- Why prefer `through('lessons')->has('submissions')` over the six-argument `hasManyThrough()`?It reuses the key names already declared on `Course::lessons()` and `Lesson::submissions()`, so custom keys live in one place and cannot drift apart. It also reads as the path it follows, and it picks `hasManyThrough` or `hasOneThrough` from the kinds of the two relations.
- Can `hasManyThrough()` reach courses from a student through the `course_student` pivot?No. Through relations chain has-type links, where each hop's foreign key sits on the next table. A pivot is a many-to-many link, so a student's courses are a `belongsToMany` relation instead.
saying these in an interview costs you the question
- The third argument of hasManyThrough is the key on the final table
- hasManyThrough runs one query per intermediate model
- Creating a submission through the course's hasManyThrough relation fills in its lesson_id
- hasManyThrough can cross a belongsToMany pivot table
- The final table needs a course_id column for hasManyThrough to work