Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Polymorphic relationships

Comments can be left on posts, on videos and on products. Keeping three nearly identical tables is awkward. A polymorphic relationship lets one model belong to models of different types.

The table

Instead of post_id the table has two columns: the owner's identifier and type. The morphs() method creates them:

Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->text('body');
    $table->morphs('commentable');
    $table->timestamps();
});

The commentable_id and commentable_type columns appear.

The models

use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class Comment extends Model
{
    protected $fillable = ['body'];
 
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}
use Illuminate\Database\Eloquent\Relations\MorphMany;
 
class Post extends Model
{
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
 
class Video extends Model
{
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

Using it

$post->comments()->create(['body' => 'Great post!']);
$video->comments()->create(['body' => 'Thanks for the video']);
 
foreach ($post->comments as $comment) {
    echo $comment->body;
}
 
$owner = $comment->commentable; // a Post or a Video

Laravel fills in the type and the identifier by itself and returns a model of the right class.

A single record

For a one-to-one relationship — say, one image for a user and for a product — there is morphOne():

public function image(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable');
}

Many to many

Tags fit both posts and videos. The links live in one pivot table, taggables, with the tag_id, taggable_id and taggable_type columns:

class Post extends Model
{
    public function tags(): MorphToMany
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}
 
class Tag extends Model
{
    public function posts(): MorphToMany
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }
}
$post->tags()->attach($tagId);
$post->tags()->sync([1, 2, 3]);

Loading without extra queries

$comments = Comment::with('commentable')->latest()->get();
 
$posts = Post::withCount('comments')->get();

The morph map

By default the type column stores the full class name — App\Models\Post. If the class is renamed, old records lose their owner. A morph map replaces the names with short labels:

use Illuminate\Database\Eloquent\Relations\Relation;
 
public function boot(): void
{
    Relation::enforceMorphMap([
        'post' => Post::class,
        'video' => Video::class,
    ]);
}

Common mistake

A polymorphic relationship cannot have a foreign key: the commentable_id column refers to different tables. The database will not delete comments together with a post, nor stop a reference to a non-existent record. Delete dependent records yourself — in the model's deleting event — and set the morph map from the very start of the project.

Thanks for reading!

Donate to the site — we work hard for you.