Advanced Eloquent: Polymorphism & Morph Maps
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
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'
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();
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")