0

Has-many-through and has-one-through

Intermediate5 min read·lv-12-008
sql

Concept

hasManyThrough and hasOneThrough allow accessing distant relationships through an intermediate model without defining the intermediate manually.

hasManyThrough($final, $through): Access records from a model that doesn't have a direct foreign key link. Example: Country hasMany Users hasMany PostsCountry hasManyThrough Posts (via Users).

hasOneThrough: Same but returns a single record.

Polymorphic relationships allow a model to belong to multiple different models using a single association. This avoids creating multiple foreign key columns or join tables.

morphTo / morphOne / morphMany: The polymorphic side has two database columns: *_type (class name) and *_id (foreign key). A Comment model can belong to either a Post or a Video using commentable_type + commentable_id.

Morph maps: Replace class names with short aliases in the database. Relation::morphMap(['post' => Post::class, 'video' => Video::class]). Defined in AppServiceProvider. This decouples the DB from class names — important for refactoring.

morphToMany / morphedByMany: Polymorphic many-to-many. A Tag can belong to both Post and Video via a single taggables pivot table.

Code Example

php
<?php
// hasManyThrough — Country → Users → Posts
class Country extends Model
{
    // Country hasManyThrough Posts via Users
    // posts.user_id → users.id; users.country_id → countries.id
    public function posts(): \Illuminate\Database\Eloquent\Relations\HasManyThrough
    {
        return $this->hasManyThrough(Post::class, User::class);
    }
}
// $country->posts — all posts from all users in that country

// Polymorphic one-to-many — Comment can belong to Post OR Video
class Comment extends Model
{
    // commentable_type = 'App\Models\Post' or 'App\Models\Video'
    // commentable_id = the post_id or video_id
    public function commentable(): \Illuminate\Database\Eloquent\Relations\MorphTo
    {
        return $this->morphTo(); // infers columns: commentable_type, commentable_id
    }
}

class Post extends Model
{
    public function comments(): \Illuminate\Database\Eloquent\Relations\MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Video extends Model
{
    public function comments(): \Illuminate\Database\Eloquent\Relations\MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

// Usage
$post = Post::find(1);
$post->comments()->create(['body' => 'Great article!', 'user_id' => 1]);

$comment = Comment::find(1);
$parent = $comment->commentable; // returns Post or Video instance

// Morph map — in AppServiceProvider::boot()
\Illuminate\Database\Eloquent\Relations\Relation::morphMap([
    'post'  => \App\Models\Post::class,
    'video' => \App\Models\Video::class,
]);
// Now commentable_type stores 'post' instead of 'App\Models\Post'

// Polymorphic many-to-many — Tags on both Posts and Videos
class Tag extends Model
{
    public function posts(): \Illuminate\Database\Eloquent\Relations\MorphToMany
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }
    public function videos(): \Illuminate\Database\Eloquent\Relations\MorphToMany
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}

class Post extends Model
{
    public function tags(): \Illuminate\Database\Eloquent\Relations\MorphToMany
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}