Navigation

Laravel

Polymorphic Relationships in Laravel: Complex Data Modeling

Master Laravel's polymorphic relationships for complex data modeling. Learn to build flexible, maintainable database schemas with practical examples for CMS, e-commerce, and multi-tenant applications.

Master Laravel's polymorphic relationships to build flexible, maintainable data models for complex applications like content management systems and e-commerce platforms.

Table Of Contents

Understanding Polymorphic Relationships

Polymorphic relationships in Laravel allow a model to belong to more than one other model on a single association. This powerful feature enables you to create flexible database schemas where related data can be associated with multiple types of parent models without duplicating table structures.

Consider a commenting system where users can comment on posts, videos, and products. Instead of creating separate comment tables for each type, polymorphic relationships let you use a single comments table that can reference any commentable model.

This approach is essential when building modular Laravel applications where different modules need to share common functionality without tight coupling.

Basic Polymorphic Relationships

One-to-Many Polymorphic

The most common polymorphic relationship is one-to-many, where one polymorphic model can belong to multiple types of parent models:

// Migration
Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->text('content');
    $table->unsignedBigInteger('user_id');
    $table->morphs('commentable'); // Creates commentable_id and commentable_type
    $table->timestamps();
    
    $table->index(['commentable_type', 'commentable_id']);
});

// Comment Model
class Comment extends Model
{
    protected $fillable = ['content', 'user_id'];
    
    public function commentable()
    {
        return $this->morphTo();
    }
    
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

// 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 examples:

// Create comments
$post = Post::find(1);
$post->comments()->create([
    'content' => 'Great article!',
    'user_id' => auth()->id(),
]);

$video = Video::find(1);
$video->comments()->create([
    'content' => 'Amazing video!',
    'user_id' => auth()->id(),
]);

// Retrieve comments with parent models
$comments = Comment::with('commentable')->get();

foreach ($comments as $comment) {
    echo "Comment on " . class_basename($comment->commentable) . ": " . $comment->content;
}

Many-to-Many Polymorphic

For more complex scenarios, you might need many-to-many polymorphic relationships. Consider a tagging system where tags can be applied to multiple types of content:

// Migrations
Schema::create('tags', function (Blueprint $table) {
    $table->id();
    $table->string('name')->unique();
    $table->string('slug')->unique();
    $table->timestamps();
});

Schema::create('taggables', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('tag_id');
    $table->morphs('taggable');
    $table->timestamps();
    
    $table->unique(['tag_id', 'taggable_id', 'taggable_type']);
    $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
});

// Tag Model
class Tag extends Model
{
    protected $fillable = ['name', 'slug'];
    
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }
    
    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
    
    public function products()
    {
        return $this->morphedByMany(Product::class, 'taggable');
    }
}

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

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

Usage:

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

// Get all content with specific tag
$tag = Tag::with(['posts', 'videos', 'products'])->find(1);
$allTaggedContent = collect()
    ->merge($tag->posts)
    ->merge($tag->videos)
    ->merge($tag->products);

Advanced Polymorphic Patterns

Polymorphic Relationships with Constraints

Add constraints to polymorphic relationships for better data integrity:

class Image extends Model
{
    protected $fillable = ['url', 'alt_text'];
    
    public function imageable()
    {
        return $this->morphTo();
    }
    
    // Scope for specific types
    public function scopeForPosts($query)
    {
        return $query->where('imageable_type', Post::class);
    }
    
    public function scopeForProducts($query)
    {
        return $query->where('imageable_type', Product::class);
    }
}

// Usage with constraints
class Post extends Model
{
    public function images()
    {
        return $this->morphMany(Image::class, 'imageable')
                   ->where('imageable_type', static::class);
    }
    
    public function featuredImage()
    {
        return $this->morphOne(Image::class, 'imageable')
                   ->where('is_featured', true);
    }
}

Custom Polymorphic Types

Instead of storing full class names, use custom polymorphic types for cleaner database storage and better performance:

// In a Service Provider (AppServiceProvider)
use Illuminate\Database\Eloquent\Relations\Relation;

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

This stores 'post' instead of 'App\Models\Post' in the database, making it more maintainable and efficient.

Nested Polymorphic Relationships

Handle complex scenarios with nested polymorphic relationships:

class Activity extends Model
{
    protected $fillable = ['action', 'description'];
    
    public function subject()
    {
        return $this->morphTo();
    }
    
    public function causer()
    {
        return $this->morphTo('causer');
    }
}

// Usage
class Post extends Model
{
    public function activities()
    {
        return $this->morphMany(Activity::class, 'subject');
    }
}

class User extends Model
{
    public function causedActivities()
    {
        return $this->morphMany(Activity::class, 'causer');
    }
}

// Create activity
Activity::create([
    'action' => 'created',
    'description' => 'User created a new post',
    'subject_type' => 'post',
    'subject_id' => $post->id,
    'causer_type' => 'user',
    'causer_id' => $user->id,
]);

Real-World Implementation Examples

Content Management System

Build a flexible CMS where different content types share common features:

// Base Content Model
abstract class Content extends Model
{
    protected $fillable = ['title', 'slug', 'status', 'published_at'];
    
    public function author()
    {
        return $this->belongsTo(User::class, 'author_id');
    }
    
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
    
    public function images()
    {
        return $this->morphMany(Image::class, 'imageable');
    }
    
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
    
    public function seoMeta()
    {
        return $this->morphOne(SeoMeta::class, 'seoable');
    }
}

// Specific Content Types
class Article extends Content
{
    protected $table = 'articles';
    protected $fillable = ['title', 'slug', 'content', 'excerpt', 'status', 'published_at'];
}

class Gallery extends Content
{
    protected $table = 'galleries';
    protected $fillable = ['title', 'slug', 'description', 'status', 'published_at'];
}

class Event extends Content
{
    protected $table = 'events';
    protected $fillable = ['title', 'slug', 'description', 'event_date', 'location', 'status', 'published_at'];
    
    protected $casts = [
        'event_date' => 'datetime',
    ];
}

E-commerce Product System

Create a flexible product system where different product types can have common attributes:

class Attribute extends Model
{
    protected $fillable = ['name', 'type', 'required'];
    
    public function values()
    {
        return $this->hasMany(AttributeValue::class);
    }
}

class AttributeValue extends Model
{
    protected $fillable = ['attribute_id', 'value'];
    
    public function attribute()
    {
        return $this->belongsTo(Attribute::class);
    }
    
    public function attributable()
    {
        return $this->morphTo();
    }
}

class Product extends Model
{
    public function attributeValues()
    {
        return $this->morphMany(AttributeValue::class, 'attributable');
    }
    
    public function addAttribute(Attribute $attribute, $value)
    {
        return $this->attributeValues()->create([
            'attribute_id' => $attribute->id,
            'value' => $value,
        ]);
    }
    
    public function getAttributeValue(string $attributeName)
    {
        return $this->attributeValues()
                   ->whereHas('attribute', function ($query) use ($attributeName) {
                       $query->where('name', $attributeName);
                   })
                   ->first()?->value;
    }
}

// Usage
$product = Product::find(1);
$colorAttribute = Attribute::where('name', 'color')->first();
$product->addAttribute($colorAttribute, 'Red');

$color = $product->getAttributeValue('color'); // Returns 'Red'

Performance Optimization

Eager Loading Polymorphic Relationships

Optimize queries when working with polymorphic relationships:

// Load all comments with their polymorphic relationships
$comments = Comment::with('commentable')->get();

// Load specific types efficiently
$comments = Comment::with([
    'commentable' => function ($morphTo) {
        $morphTo->morphWith([
            Post::class => ['author', 'category'],
            Video::class => ['channel', 'tags'],
            Product::class => ['brand', 'category'],
        ]);
    }
])->get();

Indexing for Performance

Proper indexing is crucial for polymorphic relationship performance:

Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->text('content');
    $table->unsignedBigInteger('user_id');
    $table->morphs('commentable'); // Creates commentable_id and commentable_type
    $table->timestamps();
    
    // Composite index for polymorphic queries
    $table->index(['commentable_type', 'commentable_id']);
    
    // Index for specific type queries
    $table->index(['commentable_type', 'created_at']);
});

Query Optimization

Use specific queries to improve performance:

class CommentService
{
    public function getCommentsForModel($model, int $limit = 10)
    {
        return Comment::where('commentable_type', get_class($model))
                     ->where('commentable_id', $model->id)
                     ->with('user')
                     ->latest()
                     ->limit($limit)
                     ->get();
    }
    
    public function getCommentsByType(string $type, int $limit = 50)
    {
        return Comment::where('commentable_type', $this->getMorphType($type))
                     ->with(['commentable', 'user'])
                     ->latest()
                     ->limit($limit)
                     ->get();
    }
    
    protected function getMorphType(string $type): string
    {
        $morphMap = [
            'post' => Post::class,
            'video' => Video::class,
            'product' => Product::class,
        ];
        
        return $morphMap[$type] ?? $type;
    }
}

Testing Polymorphic Relationships

Unit Tests

Test polymorphic relationships thoroughly:

class CommentTest extends TestCase
{
    public function test_comment_belongs_to_post(): void
    {
        $post = Post::factory()->create();
        $comment = Comment::factory()->create([
            'commentable_type' => Post::class,
            'commentable_id' => $post->id,
        ]);
        
        $this->assertInstanceOf(Post::class, $comment->commentable);
        $this->assertEquals($post->id, $comment->commentable->id);
    }
    
    public function test_post_has_many_comments(): void
    {
        $post = Post::factory()->create();
        $comments = Comment::factory()->count(3)->create([
            'commentable_type' => Post::class,
            'commentable_id' => $post->id,
        ]);
        
        $this->assertCount(3, $post->comments);
        $this->assertInstanceOf(Comment::class, $post->comments->first());
    }
    
    public function test_polymorphic_relationship_with_different_models(): void
    {
        $post = Post::factory()->create();
        $video = Video::factory()->create();
        
        $postComment = Comment::factory()->create([
            'commentable_type' => Post::class,
            'commentable_id' => $post->id,
        ]);
        
        $videoComment = Comment::factory()->create([
            'commentable_type' => Video::class,
            'commentable_id' => $video->id,
        ]);
        
        $this->assertInstanceOf(Post::class, $postComment->commentable);
        $this->assertInstanceOf(Video::class, $videoComment->commentable);
    }
}

Integration Tests

Test complex polymorphic scenarios:

class TaggingSystemTest extends TestCase
{
    public function test_tag_can_be_attached_to_multiple_content_types(): void
    {
        $tag = Tag::factory()->create(['name' => 'Laravel']);
        $post = Post::factory()->create();
        $video = Video::factory()->create();
        
        $post->tags()->attach($tag);
        $video->tags()->attach($tag);
        
        $this->assertTrue($post->tags->contains($tag));
        $this->assertTrue($video->tags->contains($tag));
        $this->assertCount(2, $tag->refresh()->taggables);
    }
}

Common Pitfalls and Solutions

Type Safety

Ensure type safety when working with polymorphic relationships:

trait HasComments
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
    
    public function addComment(string $content, User $user): Comment
    {
        if (!$this->exists) {
            throw new \InvalidArgumentException('Model must be saved before adding comments');
        }
        
        return $this->comments()->create([
            'content' => $content,
            'user_id' => $user->id,
        ]);
    }
}

// Use trait in models
class Post extends Model
{
    use HasComments;
}

class Video extends Model
{
    use HasComments;
}

Database Consistency

Maintain referential integrity with custom validation:

class Comment extends Model
{
    protected static function boot()
    {
        parent::boot();
        
        static::creating(function ($comment) {
            if (!$comment->commentableExists()) {
                throw new \InvalidArgumentException('Commentable model does not exist');
            }
        });
    }
    
    public function commentableExists(): bool
    {
        if (!$this->commentable_type || !$this->commentable_id) {
            return false;
        }
        
        $modelClass = $this->commentable_type;
        
        if (!class_exists($modelClass)) {
            return false;
        }
        
        return $modelClass::where('id', $this->commentable_id)->exists();
    }
}

Polymorphic relationships are powerful tools for building flexible, maintainable applications. When used correctly, they eliminate code duplication while maintaining clean, efficient database schemas. They're particularly valuable in complex Laravel applications where different entities share similar behaviors and relationships.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Laravel