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
- Basic Polymorphic Relationships
- Advanced Polymorphic Patterns
- Real-World Implementation Examples
- Performance Optimization
- Testing Polymorphic Relationships
- Common Pitfalls and Solutions
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.
Add Comment
No comments yet. Be the first to comment!