Advanced Laravel

Advanced Eloquent: Polymorphism & Morph Maps

18 min Lesson 32 of 40

Advanced Eloquent: Polymorphism & Morph Maps

Polymorphic relationships in Laravel allow a model to belong to more than one type of model on a single association. This lesson explores advanced polymorphic patterns, morph maps, and best practices for building flexible database schemas.

Understanding Polymorphic Relationships

Polymorphic relationships provide a way to create a single association that can reference multiple model types. This is useful for features like comments, likes, or images that can belong to different entities.

<?php

// Database migration for polymorphic comments
Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->text('body');
    $table->morphs('commentable'); // Creates commentable_id and commentable_type
    $table->timestamps();
});

// Comment model
class Comment extends Model
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

// Post model
class Post extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

// Video model
class Video extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

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

$video = Video::find(1);
$video->comments()->create(['body' => 'Amazing video!']);

// Retrieve the parent
$comment = Comment::find(1);
$parent = $comment->commentable; // Returns Post or Video instance
Tip: The morphs() method automatically creates both the ID column (commentable_id) and the type column (commentable_type). You can also use nullableMorphs() for optional relationships.

Morph Maps: Decoupling Models from Database

By default, Laravel stores the fully qualified class name in the type column (e.g., "App\Models\Post"). Morph maps allow you to use shorter aliases, making your database more maintainable and resilient to namespace changes.

<?php

// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Relations\Relation;

public function boot()
{
    Relation::enforceMorphMap([
        'post' => 'App\Models\Post',
        'video' => 'App\Models\Video',
        'user' => 'App\Models\User',
    ]);
}

// Database will now store 'post' instead of 'App\Models\Post'
// This makes your code more maintainable and database-agnostic

// Retrieving morph map
$map = Relation::morphMap();

// Getting the alias for a model
$alias = Relation::getMorphAlias(Post::class); // Returns 'post'
Warning: If you add a morph map after you've already stored data using full class names, you'll need to migrate your existing data. Always implement morph maps at the beginning of your project to avoid migration issues.

Many-to-Many Polymorphic Relations

Many-to-many polymorphic relationships allow a model to belong to multiple types of models on a shared relationship. This is perfect for features like tags or categories that can be applied to various content types.

<?php

// Database migration for taggables
Schema::create('tags', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->timestamps();
});

Schema::create('taggables', function (Blueprint $table) {
    $table->foreignId('tag_id')->constrained()->onDelete('cascade');
    $table->morphs('taggable');
    $table->timestamps();
});

// Tag model
class Tag extends Model
{
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }

    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}

// Post model
class Post extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

// Video model
class Video extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

// Usage
$post = Post::find(1);
$post->tags()->attach([1, 2, 3]);

$tag = Tag::find(1);
$allPosts = $tag->posts; // All posts with this tag
$allVideos = $tag->videos; // All videos with this tag

// Detach tags
$post->tags()->detach([2, 3]);

// Sync tags (attach new, detach missing)
$post->tags()->sync([1, 4, 5]);

Custom Polymorphic Types

You can create custom polymorphic relationships beyond the standard patterns by defining custom methods and using the relationship's query builder.

<?php

// One-to-one polymorphic relationship
class Image extends Model
{
    public function imageable()
    {
        return $this->morphTo();
    }
}

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

class Product extends Model
{
    public function images()
    {
        return $this->morphMany(Image::class, 'imageable');
    }
}

// Custom polymorphic query scopes
class Comment extends Model
{
    public function commentable()
    {
        return $this->morphTo();
    }

    public function scopeForPosts($query)
    {
        return $query->where('commentable_type', 'post');
    }

    public function scopeForVideos($query)
    {
        return $query->where('commentable_type', 'video');
    }
}

// Usage
$postComments = Comment::forPosts()->get();
$videoComments = Comment::forVideos()->get();

// Eager loading polymorphic relationships
$comments = Comment::with('commentable')->get();

// Constrained eager loading
$comments = Comment::with([
    'commentable' => function ($query) {
        $query->where('status', 'published');
    }
])->get();

// Polymorphic eager loading constraints
$posts = Post::with([
    'comments' => function ($query) {
        $query->where('approved', true);
    }
])->get();
Note: When using polymorphic relationships, always consider indexing your morph columns (both _id and _type) for better query performance, especially on large datasets.

Advanced Polymorphic Patterns

<?php

// Nested polymorphic relationships
class Activity extends Model
{
    public function subject()
    {
        return $this->morphTo();
    }

    public function causer()
    {
        return $this->morphTo();
    }
}

// Log activity with multiple polymorphic relations
Activity::create([
    'subject_type' => 'post',
    'subject_id' => 1,
    'causer_type' => 'user',
    'causer_id' => 5,
    'description' => 'Post was published',
]);

// Polymorphic relations with pivot data
class User extends Model
{
    public function favoriteables()
    {
        return $this->hasMany(Favorite::class);
    }

    public function favoritePosts()
    {
        return $this->morphedByMany(Post::class, 'favoriteable')
            ->withPivot('favorited_at', 'notes')
            ->withTimestamps();
    }
}

// Attach with pivot data
$user->favoritePosts()->attach($postId, [
    'favorited_at' => now(),
    'notes' => 'Really helpful article',
]);

// Retrieve pivot data
$user->favoritePosts->each(function ($post) {
    echo $post->pivot->favorited_at;
    echo $post->pivot->notes;
});

Exercise 1: Polymorphic Likes System

Create a polymorphic likes system that allows users to like posts, comments, and videos. Implement morph maps and add methods to:

  • Check if a user has liked an item
  • Get the total likes count for any likeable item
  • Get all items a user has liked grouped by type
  • Unlike an item

Exercise 2: Activity Log

Build an activity log system using polymorphic relationships that tracks:

  • The subject of the activity (what was changed)
  • The causer of the activity (who made the change)
  • Store before/after states in JSON columns
  • Create a dashboard method to retrieve all activities for a user
  • Implement filtering by activity type and date range

Exercise 3: Advanced Tagging System

Create a many-to-many polymorphic tagging system with the following features:

  • Tags can be applied to posts, videos, and products
  • Store tagging metadata (who tagged, when, context)
  • Implement a method to get the most popular tags across all types
  • Create a search method that finds items by tag across all types
  • Add tag categories (e.g., "technical", "marketing", "design")