Polymorphic در Laravel؛ راهنمای کامل روابط چندشکلی
Polymorphic در Laravel یکی از قابلیتهای قدرتمند Eloquent برای طراحی رابطههای انعطافپذیر بین مدلهاست. با روابط چندشکلی یا Polymorphic Relationships میتوان یک مدل را به چند نوع مدل مختلف متصل کرد؛ برای مثال یک جدول comments میتواند هم برای مقالهها، هم محصولات، هم ویدئوها و هم تیکتها کامنت نگه دارد. در این مقاله، بهصورت کامل و فنی با مفهوم Polymorphic در Laravel، انواع روابط morphOne، morphMany، morphTo، morphToMany، morphedByMany، طراحی Migration، مثالهای واقعی، Performance، Eager Loading، Morph Map، خطاهای رایج و بهترین روشهای استفاده در پروژههای شرکتی آشنا میشویم.
برای شنیدن متن، روی «پخش صوت مقاله» بزنید.
مقدمه
در توسعه نرمافزارهای واقعی، مدلهای دیتابیس همیشه رابطههای ساده و مستقیم ندارند. گاهی یک موجودیت باید بتواند به چند نوع موجودیت مختلف متصل شود. برای مثال، فرض کنید در یک پروژه Laravel میخواهید سیستم کامنت طراحی کنید. کاربران باید بتوانند برای مقالهها، محصولات، ویدئوها و حتی تیکتها کامنت بگذارند. یک راه ساده اما ضعیف این است که برای هرکدام جدول جداگانه بسازیم؛ مثل post_comments، product_comments، video_comments و ticket_comments. این روش در ابتدا قابل فهم است، اما خیلی زود باعث تکرار ساختار، پیچیدگی Queryها، افزایش Migrationها و سخت شدن توسعه میشود.
راه حرفهایتر استفاده از Polymorphic Relationship یا رابطه چندشکلی در Eloquent است. در این مدل، یک جدول عمومی مثل comments ساخته میشود و هر کامنت میتواند به هر مدل قابل کامنتگذاری متصل شود. یعنی یک کامنت ممکن است متعلق به Post باشد، کامنت دیگر متعلق به Product و کامنت بعدی متعلق به Video.
طبق مستندات رسمی Laravel، رابطه یکبهچند Polymorphic شبیه رابطه معمولی یکبهچند است، با این تفاوت که مدل فرزند میتواند از طریق یک ارتباط واحد به بیش از یک نوع مدل والد متعلق باشد. مطالعه مستندات رسمی Laravel درباره روابط Eloquent
این قابلیت برای شرکتهای تولید نرمافزار اهمیت زیادی دارد، چون بسیاری از سیستمهای سازمانی و تجاری با موجودیتهای قابل اشتراک سروکار دارند؛ مثل کامنت، فایل، تصویر، تگ، لاگ فعالیت، نوتیفیکیشن، لایک، امتیازدهی، ضمیمهها و تاریخچه تغییرات. اگر این بخشها درست طراحی نشوند، پروژه بهمرور با جدولهای تکراری، منطق پراکنده و هزینه نگهداری بالا مواجه میشود.
در این مقاله، بهصورت کامل و فنی بررسی میکنیم Polymorphic در Laravel چیست، چه زمانی باید از آن استفاده کنیم، چه انواعی دارد، چطور Migration و Modelها را طراحی کنیم، چه تفاوتی با رابطههای معمولی دارد، در چه سناریوهایی خطرناک یا نامناسب است و چگونه در پروژههای حرفهای Laravel از آن به شکل بهینه و قابل نگهداری استفاده کنیم. 🚀
Polymorphic در Laravel چیست؟
Polymorphic Relationship در Laravel نوعی رابطه Eloquent است که اجازه میدهد یک مدل به چند نوع مدل مختلف متصل شود. در رابطههای معمولی، یک مدل فرزند معمولاً فقط به یک نوع مدل والد وابسته است. برای مثال، هر کامنت فقط به یک مقاله متصل میشود:
posts
comments
و جدول comments ستونی مثل post_id دارد.
اما اگر همان کامنت قرار باشد به مقاله، محصول، ویدئو و تیکت متصل شود، رابطه معمولی کافی نیست. در رابطه Polymorphic، بهجای یک ستون مثل post_id، معمولاً دو ستون داریم:
commentable_id
commentable_type
معنی این دو ستون:
| ستون | کاربرد |
|---|---|
| commentable_id | شناسه رکورد والد، مثلاً ID مقاله یا محصول |
| commentable_type | نوع مدل والد، مثلاً App\Models\Post یا App\Models\Product |
بنابراین یک ردیف در جدول comments میتواند اینگونه باشد:
| id | body | commentable_id | commentable_type |
|---|---|---|---|
| 1 | متن کامنت | 15 | App\Models\Post |
| 2 | متن کامنت | 8 | App\Models\Product |
| 3 | متن کامنت | 4 | App\Models\Video |
اینجا هر سه رکورد در یک جدول هستند، اما به مدلهای متفاوتی وصل شدهاند.
چرا از Polymorphic Relationship استفاده میکنیم؟
روابط Polymorphic برای جلوگیری از تکرار ساختار دیتابیس و کد استفاده میشوند. فرض کنید میخواهید سیستم فایل پیوست داشته باشید. فایلها ممکن است به این مدلها متصل شوند:
- کاربر
- محصول
- مقاله
- تیکت
- فاکتور
- سفارش
- قرارداد
اگر برای هرکدام جدول جداگانه بسازید، ساختار دیتابیس پیچیده و تکراری میشود:
user_files
product_files
post_files
ticket_files
invoice_files
order_files
contract_files
اما با Polymorphic میتوانید یک جدول عمومی داشته باشید:
attachments
attachable_id
attachable_type
مزایای مهم Polymorphic:
| مزیت | توضیح |
|---|---|
| کاهش تکرار جدولها | یک جدول مشترک برای چند مدل |
| کاهش تکرار کد | رابطه مشابه در چند مدل استفاده میشود |
| توسعهپذیری بالا | اضافه کردن مدل جدید سادهتر است |
| طراحی انعطافپذیر | مناسب موجودیتهای عمومی مثل کامنت، فایل، تگ |
| هماهنگی با Eloquent | Laravel متدهای آماده مثل morphTo و morphMany دارد |
| مناسب معماری شرکتی | ماژولهای عمومی راحتتر طراحی میشوند |
اما این رابطه همیشه بهترین انتخاب نیست. در بخشهای بعدی توضیح میدهیم چه زمانی استفاده از Polymorphic مناسب است و چه زمانی بهتر است رابطه معمولی طراحی شود.
مثال ساده Polymorphic: سیستم کامنت
فرض کنید در یک سایت شرکتی یا فروشگاهی میخواهید کاربران بتوانند روی مقالهها و محصولات کامنت بگذارند.
مدلها:
Post
Product
Comment
هر Post چند کامنت دارد. هر Product هم چند کامنت دارد. اما هر Comment فقط به یک والد متصل است؛ یا مقاله یا محصول.
ساختار جدول comments:
id
body
commentable_id
commentable_type
created_at
updated_at
در اینجا نام رابطه را commentable گذاشتهایم؛ یعنی چیزی که قابلیت کامنت گرفتن دارد.
ساخت Migration برای رابطه Polymorphic
برای ساخت جدول کامنتها:
php artisan make:migration create_comments_table
نمونه Migration:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('body');
$table->morphs('commentable');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('comments');
}
};
متد morphs('commentable') معمولاً دو ستون ایجاد میکند:
commentable_id
commentable_type
طبق مستندات رسمی Laravel درباره Migrationها، Schema Builder لاراول امکاناتی برای ساخت و تغییر جدولها بهصورت مستقل از نوع دیتابیس ارائه میدهد و در روابط Polymorphic نیز ستونهای مربوط به Morph از طریق متدهایی مثل morphs قابل ایجاد هستند. مطالعه مستندات رسمی Laravel درباره Migrations
اگر رابطه اختیاری باشد، میتوانید از nullableMorphs استفاده کنید:
$table->nullableMorphs('commentable');
برای UUID یا ULID نیز متدهای مخصوص وجود دارد؛ مانند:
$table->uuidMorphs('commentable');
$table->ulidMorphs('commentable');
انتخاب نوع ستون باید با نوع کلید اصلی مدلهای شما هماهنگ باشد.
تعریف رابطه در مدل Comment با morphTo
در مدل Comment باید مشخص کنیم که این کامنت میتواند به مدلهای مختلفی متعلق باشد. برای این کار از morphTo استفاده میکنیم.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Comment extends Model
{
protected $fillable = [
'body',
];
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
متد commentable رابطه معکوس Polymorphic است. یعنی از روی یک کامنت میتوانیم والد آن را پیدا کنیم:
$comment = Comment::find(1);
$parent = $comment->commentable;
اگر commentable_type برابر App\Models\Post باشد، $parent یک مدل Post خواهد بود. اگر برابر App\Models\Product باشد، $parent یک مدل Product خواهد بود.
تعریف رابطه در مدل Post با morphMany
حالا باید در مدل Post مشخص کنیم که هر مقاله میتواند چند کامنت داشته باشد:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Post extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
در مدل Product نیز همین رابطه را تعریف میکنیم:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Product extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
حالا هم مقاله و هم محصول میتوانند کامنت داشته باشند، بدون اینکه جدولهای جداگانه بسازیم.
ایجاد رکورد در رابطه Polymorphic
برای ساخت کامنت برای یک مقاله:
$post = Post::findOrFail(1);
$post->comments()->create([
'body' => 'این مقاله بسیار کاربردی بود.',
]);
Laravel بهصورت خودکار مقدارهای زیر را پر میکند:
commentable_id = ID مقاله
commentable_type = کلاس مدل Post
برای محصول:
$product = Product::findOrFail(1);
$product->comments()->create([
'body' => 'کیفیت این محصول عالی است.',
]);
اینجا نیز Laravel بهصورت خودکار commentable_id و commentable_type را بر اساس مدل Product پر میکند.
خواندن دادهها در رابطه Polymorphic
دریافت کامنتهای مقاله:
$post = Post::with('comments')->findOrFail(1);
foreach ($post->comments as $comment) {
echo $comment->body;
}
دریافت والد یک کامنت:
$comment = Comment::findOrFail(1);
$parent = $comment->commentable;
اگر والد یک مقاله باشد:
echo $comment->commentable->title;
اما باید توجه کنید که والد ممکن است انواع مختلفی داشته باشد. بنابراین در کدهای حرفهای بهتر است نوع آن را بررسی کنید:
if ($comment->commentable instanceof Post) {
echo $comment->commentable->title;
}
if ($comment->commentable instanceof Product) {
echo $comment->commentable->name;
}
انواع روابط Polymorphic در Laravel
Laravel چند نوع رابطه Polymorphic ارائه میدهد. مهمترین آنها:
| نوع رابطه | متدها | مثال |
|---|---|---|
| یکبهیک Polymorphic | morphOne, morphTo | تصویر اصلی برای کاربر یا محصول |
| یکبهچند Polymorphic | morphMany, morphTo | کامنتها برای مقاله و محصول |
| چندبهچند Polymorphic | morphToMany, morphedByMany | تگها برای مقاله و ویدئو |
| One of Many Polymorphic | latestOfMany, oldestOfMany, ofMany | آخرین تصویر یا بهترین قیمت مرتبط |
مستندات رسمی Laravel انواع مختلف روابط Eloquent را پوشش میدهد، از جمله One To One Polymorphic، One To Many Polymorphic و Many To Many Polymorphic. مطالعه مستندات رسمی Laravel درباره روابط چندشکلی
رابطه یکبهیک Polymorphic با morphOne
رابطه یکبهیک Polymorphic زمانی کاربرد دارد که یک مدل مرتبط بتواند متعلق به چند نوع مدل باشد، اما هر والد فقط یک رکورد از آن داشته باشد.
مثال: هر کاربر، محصول یا مقاله میتواند یک تصویر اصلی داشته باشد.
مدلها:
User
Product
Post
Image
جدول images:
Schema::create('images', function (Blueprint $table) {
$table->id();
$table->string('path');
$table->morphs('imageable');
$table->timestamps();
});
مدل Image:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Image extends Model
{
protected $fillable = [
'path',
];
public function imageable(): MorphTo
{
return $this->morphTo();
}
}
مدل Product:
use Illuminate\Database\Eloquent\Relations\MorphOne;
class Product extends Model
{
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}
مدل Post:
use Illuminate\Database\Eloquent\Relations\MorphOne;
class Post extends Model
{
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}
ایجاد تصویر برای محصول:
$product->image()->create([
'path' => 'products/product-1.jpg',
]);
دریافت تصویر:
$image = $product->image;
این روش برای Avatar، تصویر شاخص، فایل اصلی یا تنظیمات تکردیفی مشترک کاربرد دارد.
رابطه یکبهچند Polymorphic با morphMany
رابطه یکبهچند Polymorphic رایجترین نوع رابطه چندشکلی است.
مثالهای رایج:
- یک مقاله چند کامنت دارد.
- یک محصول چند کامنت دارد.
- یک تیکت چند فایل پیوست دارد.
- یک کاربر چند لاگ فعالیت دارد.
- یک فاکتور چند فایل ضمیمه دارد.
نمونه فایل پیوست:
Schema::create('attachments', function (Blueprint $table) {
$table->id();
$table->string('file_name');
$table->string('file_path');
$table->morphs('attachable');
$table->timestamps();
});
مدل Attachment:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Attachment extends Model
{
protected $fillable = [
'file_name',
'file_path',
];
public function attachable(): MorphTo
{
return $this->morphTo();
}
}
مدل Ticket:
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Ticket extends Model
{
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
}
مدل Invoice:
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Invoice extends Model
{
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
}
استفاده:
$ticket->attachments()->create([
'file_name' => 'error-screenshot.png',
'file_path' => 'tickets/error-screenshot.png',
]);
رابطه چندبهچند Polymorphic با morphToMany
رابطه چندبهچند Polymorphic کمی پیچیدهتر است. این رابطه زمانی کاربرد دارد که چند مدل مختلف بتوانند با یک مدل مشترک رابطه چندبهچند داشته باشند.
مثال معروف: سیستم تگ.
مدلها:
Post
Video
Tag
هر مقاله چند تگ دارد. هر ویدئو هم چند تگ دارد. هر تگ نیز میتواند به چند مقاله و چند ویدئو متصل باشد.
جدولها:
posts
videos
tags
taggables
جدول واسط taggables:
Schema::create('taggables', function (Blueprint $table) {
$table->id();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->morphs('taggable');
$table->timestamps();
$table->unique(['tag_id', 'taggable_id', 'taggable_type']);
});
مدل Tag:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphedByMany;
class Tag extends Model
{
protected $fillable = [
'name',
'slug',
];
public function posts(): MorphedByMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
public function videos(): MorphedByMany
{
return $this->morphedByMany(Video::class, 'taggable');
}
}
مدل Post:
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Post extends Model
{
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
مدل Video:
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Video extends Model
{
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
اتصال تگ به مقاله:
$post->tags()->attach($tagId);
همگامسازی تگها:
$post->tags()->sync([1, 2, 3]);
دریافت تگهای ویدئو:
$tags = $video->tags;
در مستندات Laravel، رابطه Many To Many Polymorphic با مثال Post، Video و Tag توضیح داده شده و برای این سناریو از morphToMany و morphedByMany استفاده میشود. مطالعه مستندات رسمی Laravel درباره Many To Many Polymorphic
تفاوت morphToMany و morphedByMany
در رابطه چندبهچند Polymorphic، دو سمت رابطه متفاوت تعریف میشوند.
| متد | کجا استفاده میشود؟ | مثال |
|---|---|---|
| morphToMany | در مدلهایی که تگپذیر هستند | Post, Video |
| morphedByMany | در مدل مشترک یا هدف رابطه | Tag |
در Post:
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
در Tag:
public function posts(): MorphedByMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
یعنی مقاله میگوید «من چند تگ دارم»، و تگ میگوید «من به چند مقاله متصل هستم».
Morph Map در Laravel چیست؟
بهصورت پیشفرض، Laravel در ستون *_type نام کامل کلاس مدل را ذخیره میکند. مثلاً:
App\Models\Post
App\Models\Product
این کار ساده است، اما چند مشکل دارد:
- اگر Namespace مدل تغییر کند، دادههای قدیمی مشکل پیدا میکنند.
- مقدار ذخیرهشده طولانی است.
- ساختار داخلی کد در دیتابیس ذخیره میشود.
- وابستگی دیتابیس به نام کلاسها زیاد میشود.
برای حل این مشکل میتوان از Morph Map استفاده کرد. با Morph Map میتوانید نامهای کوتاه و پایدار تعریف کنید:
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::enforceMorphMap([
'post' => \App\Models\Post::class,
'product' => \App\Models\Product::class,
'video' => \App\Models\Video::class,
]);
بعد از این تنظیم، در ستون commentable_type بهجای App\Models\Post مقدار post ذخیره میشود.
این روش برای پروژههای حرفهای بسیار توصیه میشود، مخصوصاً اگر دیتابیس بلندمدت، API عمومی یا احتمال Refactor Namespace وجود دارد.
محل مناسب برای تعریف Morph Map معمولاً یکی از Service Providerهاست، مثل AppServiceProvider:
use Illuminate\Database\Eloquent\Relations\Relation;
public function boot(): void
{
Relation::enforceMorphMap([
'post' => \App\Models\Post::class,
'product' => \App\Models\Product::class,
]);
}
Eager Loading در روابط Polymorphic
مثل سایر رابطههای Eloquent، اگر روابط Polymorphic را بدون Eager Loading استفاده کنید، ممکن است با مشکل N+1 Query روبهرو شوید.
مثال بد:
$comments = Comment::latest()->get();
foreach ($comments as $comment) {
echo $comment->commentable->title;
}
در این حالت، برای هر کامنت ممکن است یک Query جداگانه برای والد اجرا شود.
روش بهتر:
$comments = Comment::with('commentable')->latest()->get();
اما چون commentable میتواند چند نوع مدل باشد، Laravel باید برای هر نوع مدل Query جداگانه اجرا کند. مثلاً اگر کامنتها به Post و Product متصل باشند، معمولاً Queryهای جدا برای Postها و Productها اجرا میشود.
برای سناریوهای پیچیدهتر، میتوانید روی نوعهای مختلف محدودیت اعمال کنید.
morphWith برای Eager Loading تو در تو
فرض کنید کامنتها به Post و Product متصل هستند. برای Post میخواهید نویسنده را هم Eager Load کنید و برای Product میخواهید دستهبندی را بگیرید.
در رابطه morphTo میتوان از morphWith استفاده کرد:
use Illuminate\Database\Eloquent\Relations\MorphTo;
$comments = Comment::query()
->with([
'commentable' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Post::class => ['author'],
Product::class => ['category'],
]);
},
])
->get();
این روش برای کاهش Queryهای اضافی در پروژههای بزرگ بسیار مهم است.
Query روی رابطه Polymorphic با whereHasMorph
گاهی میخواهید روی والدهای Polymorphic شرط بگذارید. مثلاً کامنتهایی را پیدا کنید که والد آنها مقاله منتشرشده یا محصول فعال است.
Laravel برای این کار متدهایی مثل whereHasMorph ارائه میدهد.
نمونه:
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Product::class],
function ($query) {
$query->where('status', 'published');
}
)->get();
اگر شرط برای هر نوع متفاوت باشد:
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Product::class],
function ($query, string $type) {
if ($type === Post::class) {
$query->where('is_published', true);
}
if ($type === Product::class) {
$query->where('is_active', true);
}
}
)->get();
این قابلیت برای پنلهای مدیریتی، فیلترهای جستجو و گزارشگیری کاربرد دارد.
one of many در روابط Polymorphic
گاهی یک مدل چند رکورد مرتبط دارد، اما شما فقط یکی از آنها را میخواهید؛ مثلاً آخرین تصویر، قدیمیترین فایل یا تصویر اصلی.
Laravel برای رابطههای Has One of Many امکاناتی مثل latestOfMany، oldestOfMany و ofMany ارائه میدهد و این مفهوم میتواند در برخی روابط Polymorphic نیز استفاده شود. در مستندات رسمی Laravel، بخش One of Many و روابط Polymorphic توضیح میدهد که میتوان از این قابلیت برای دریافت یک مدل خاص از میان چند مدل مرتبط استفاده کرد. مطالعه مستندات رسمی Laravel درباره One of Many
مثال:
use Illuminate\Database\Eloquent\Relations\MorphOne;
class Product extends Model
{
public function latestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}
}
یا دریافت تصویر اصلی بر اساس یک ستون:
public function primaryImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')
->ofMany('id', 'max', function ($query) {
$query->where('is_primary', true);
});
}
این قابلیت برای گالری تصاویر، فایلهای نسخهبندیشده و رکوردهای تاریخی بسیار مفید است.
طراحی دیتابیس در Polymorphic
طراحی دیتابیس در روابط Polymorphic باید با دقت انجام شود؛ چون برخلاف Foreign Keyهای معمولی، ستون *_type میتواند به چند جدول مختلف اشاره کند و دیتابیس بهصورت مستقیم نمیتواند روی آن Foreign Key ساده تعریف کند.
نکات مهم طراحی
| نکته | توضیح |
|---|---|
| Index ترکیبی | روی *_type و *_id Index داشته باشید |
| Morph Map | از نامهای کوتاه و پایدار استفاده کنید |
| نوع ID هماهنگ | اگر مدلها UUID دارند، از uuidMorphs استفاده کنید |
| حذف دادهها | حذف والدها را با Observer، Event یا منطق Service مدیریت کنید |
| Unique Constraint | در Pivotهای Polymorphic از Unique مناسب استفاده کنید |
| Soft Delete | اگر والد Soft Delete دارد، رفتار فرزندها را مشخص کنید |
نمونه Index:
$table->index(['commentable_type', 'commentable_id']);
متد morphs معمولاً Index ایجاد میکند، اما در طراحیهای سفارشی بهتر است این موضوع را بررسی کنید.
محدودیت مهم: Foreign Key مستقیم نداریم
یکی از مهمترین نکات روابط Polymorphic این است که دیتابیس نمیتواند بهسادگی برای commentable_id یک Foreign Key واحد تعریف کند؛ چون این ستون ممکن است به جدولهای مختلفی اشاره کند.
برای مثال:
commentable_id = 1
commentable_type = posts
یا:
commentable_id = 1
commentable_type = products
یک ستون نمیتواند همزمان Foreign Key به posts.id و products.id باشد. بنابراین حفظ یکپارچگی داده بیشتر بر عهده برنامه، Eloquent، تستها و منطق حذف/بهروزرسانی است.
این محدودیت باعث میشود Polymorphic برای همه سناریوها مناسب نباشد. اگر یک رابطه بسیار حساس مالی یا حقوقی دارید و نیاز به Constraint سخت دیتابیس دارید، باید با دقت بیشتری تصمیم بگیرید.
حذف دادههای مرتبط در Polymorphic
اگر یک مقاله حذف شود، با کامنتهای آن چه باید کرد؟ این سؤال در رابطههای Polymorphic مهم است، چون Cascade Delete دیتابیس بهسادگی قابل استفاده نیست.
روشهای رایج:
1. حذف در Model Event یا Observer
class Post extends Model
{
protected static function booted(): void
{
static::deleting(function (Post $post) {
$post->comments()->delete();
});
}
}
2. حذف در Service
DB::transaction(function () use ($post) {
$post->comments()->delete();
$post->delete();
});
3. Soft Delete
اگر داده حساس است، بهتر است والد و فرزندها Soft Delete شوند.
4. نگهداشتن داده برای Audit
در سیستمهای سازمانی، شاید لازم باشد کامنتها یا لاگها حتی بعد از حذف والد حفظ شوند. در این حالت باید رفتار نمایش و Query مشخص شود.
برای پروژههای حرفهای، بهتر است سیاست حذف دادهها برای هر رابطه Polymorphic بهصورت واضح مستند شود.
مثال کاربردی: سیستم فایل پیوست چندشکلی
در نرمافزارهای شرکتی، فایلها معمولاً به موجودیتهای مختلف وصل میشوند:
- تیکت
- فاکتور
- قرارداد
- پروژه
- کاربر
- سفارش
بهجای ساخت جدول جدا برای هرکدام، میتوان از Polymorphic استفاده کرد.
Migration:
Schema::create('attachments', function (Blueprint $table) {
$table->id();
$table->string('disk')->default('public');
$table->string('file_name');
$table->string('file_path');
$table->string('mime_type')->nullable();
$table->unsignedBigInteger('size')->nullable();
$table->morphs('attachable');
$table->timestamps();
});
مدل Attachment:
class Attachment extends Model
{
protected $fillable = [
'disk',
'file_name',
'file_path',
'mime_type',
'size',
];
public function attachable(): MorphTo
{
return $this->morphTo();
}
}
مدل Ticket:
class Ticket extends Model
{
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
}
مدل Invoice:
class Invoice extends Model
{
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
}
استفاده:
$invoice->attachments()->create([
'disk' => 'private',
'file_name' => 'invoice.pdf',
'file_path' => 'invoices/2026/invoice.pdf',
'mime_type' => 'application/pdf',
'size' => 245000,
]);
این ساختار برای پنلهای مدیریتی، CRM، ERP و سیستمهای اتوماسیون بسیار کاربردی است.
مثال کاربردی: سیستم Activity Log
یکی دیگر از کاربردهای عالی Polymorphic، ثبت لاگ فعالیت برای موجودیتهای مختلف است.
جدول:
Schema::create('activity_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('action');
$table->json('properties')->nullable();
$table->morphs('subject');
$table->timestamps();
});
مدل ActivityLog:
class ActivityLog extends Model
{
protected $fillable = [
'user_id',
'action',
'properties',
];
protected function casts(): array
{
return [
'properties' => 'array',
];
}
public function subject(): MorphTo
{
return $this->morphTo();
}
}
مدل Order:
class Order extends Model
{
public function activities(): MorphMany
{
return $this->morphMany(ActivityLog::class, 'subject');
}
}
ثبت لاگ:
$order->activities()->create([
'user_id' => auth()->id(),
'action' => 'order_created',
'properties' => [
'total_price' => $order->total_price,
],
]);
این طراحی باعث میشود لاگ فعالیتها برای سفارش، فاکتور، کاربر، محصول و سایر مدلها در یک جدول یکپارچه ذخیره شود.
مثال کاربردی: سیستم Like چندشکلی
فرض کنید کاربران بتوانند مقاله، محصول و کامنت را Like کنند.
Migration:
Schema::create('likes', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->morphs('likeable');
$table->timestamps();
$table->unique(['user_id', 'likeable_id', 'likeable_type']);
});
مدل Like:
class Like extends Model
{
protected $fillable = [
'user_id',
];
public function likeable(): MorphTo
{
return $this->morphTo();
}
}
مدل Post:
class Post extends Model
{
public function likes(): MorphMany
{
return $this->morphMany(Like::class, 'likeable');
}
}
Like کردن:
$post->likes()->firstOrCreate([
'user_id' => auth()->id(),
]);
تعداد Likeها:
$count = $post->likes()->count();
این طراحی از ایجاد جدولهای جدا مثل post_likes و product_likes جلوگیری میکند.
Polymorphic و API Resource
وقتی داده Polymorphic را در API خروجی میدهید، باید ساختار پاسخ را دقیق طراحی کنید. چون والد رابطه میتواند مدلهای مختلفی باشد.
مثال:
class CommentResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'body' => $this->body,
'commentable' => [
'type' => class_basename($this->commentable_type),
'id' => $this->commentable_id,
'title' => $this->whenLoaded('commentable', function () {
return $this->commentable->title ?? $this->commentable->name ?? null;
}),
],
];
}
}
در APIهای حرفهای بهتر است بهجای خروجی دادن مستقیم نام کلاس PHP، از نوعهای پایدارتر استفاده کنید:
{
"type": "post",
"id": 12
}
اینجا Morph Map نیز کمک میکند.
Polymorphic و Performance
روابط Polymorphic قدرتمند هستند، اما اگر بدون دقت استفاده شوند، میتوانند Performance را تحت تأثیر قرار دهند.
نکات مهم Performance
| نکته | توضیح |
|---|---|
| Eager Loading | برای جلوگیری از N+1 از with استفاده کنید |
| Index مناسب | روی ستونهای morph Index داشته باشید |
| Morph Map | مقدار type را کوتاهتر و پایدارتر میکند |
| محدود کردن نوعها | در Queryها نوعهای مورد نیاز را مشخص کنید |
| Queryهای سنگین | گزارشگیریهای پیچیده را جدا طراحی کنید |
| Cache | برای دادههای پرتکرار از Cache استفاده کنید |
| Pagination | لیستهای بزرگ را paginate کنید |
مثال:
$comments = Comment::with('commentable')
->latest()
->paginate(20);
اگر حجم داده زیاد است، از Queryهای بدون محدودیت مثل all() پرهیز کنید.
Polymorphic و تستنویسی
برای رابطههای Polymorphic باید تست بنویسید تا مطمئن شوید رابطهها درست کار میکنند.
نمونه تست:
public function test_post_can_have_comments(): void
{
$post = Post::factory()->create();
$post->comments()->create([
'body' => 'Test comment',
]);
$this->assertCount(1, $post->comments);
$this->assertDatabaseHas('comments', [
'body' => 'Test comment',
'commentable_id' => $post->id,
'commentable_type' => Post::class,
]);
}
اگر از Morph Map استفاده میکنید، مقدار commentable_type ممکن است post باشد، نه نام کامل کلاس:
$this->assertDatabaseHas('comments', [
'commentable_type' => 'post',
]);
تستهای مهم:
- ایجاد رابطه از والد
- خواندن والد از فرزند
- حذف والد و رفتار فرزند
- Eager Loading
- Query با whereHasMorph
- عملکرد Morph Map
- Unique Constraint در Pivotهای Polymorphic
Polymorphic در پروژههای شرکتی
در پروژههای شرکتی، روابط Polymorphic معمولاً در ماژولهای عمومی و قابل استفاده مجدد کاربرد دارند.
| ماژول | مدل Polymorphic | مدلهای والد احتمالی |
|---|---|---|
| کامنت | Comment | مقاله، محصول، تیکت، پروژه |
| فایل پیوست | Attachment | فاکتور، قرارداد، تیکت، کاربر |
| تصویر | Image | محصول، مقاله، کاربر |
| لاگ فعالیت | ActivityLog | سفارش، فاکتور، کاربر، محصول |
| تگ | Tag | مقاله، ویدئو، محصول |
| لایک | Like | مقاله، کامنت، محصول |
| نوتیفیکیشن داخلی | Notification | سفارش، تیکت، پرداخت |
| امتیازدهی | Rating | محصول، دوره، سرویس |
| یادداشت | Note | مشتری، پروژه، قرارداد |
| تاریخچه تغییرات | Revision | هر مدل مهم سازمانی |
این طراحی باعث میشود ماژولهای عمومی مثل فایل، کامنت و لاگ فقط یک بار پیادهسازی شوند و در بخشهای مختلف سیستم استفاده شوند.
چه زمانی از Polymorphic استفاده نکنیم؟
با وجود مزایای زیاد، Polymorphic همیشه بهترین گزینه نیست.
1. وقتی فقط دو مدل با منطق کاملاً متفاوت دارید
اگر رابطهها فقط ظاهراً شبیه هستند اما قوانین، فیلدها و رفتارهای کاملاً متفاوت دارند، جدول مشترک ممکن است باعث پیچیدگی شود.
2. وقتی نیاز جدی به Foreign Key دیتابیس دارید
در رابطه Polymorphic، Foreign Key مستقیم برای چند جدول مختلف ساده نیست. اگر یکپارچگی سخت دیتابیس بسیار مهم است، باید با دقت تصمیم بگیرید.
3. وقتی Queryهای تحلیلی بسیار پیچیده دارید
گزارشگیری روی چند نوع مدل مختلف در یک رابطه Polymorphic میتواند پیچیدهتر شود.
4. وقتی نوعهای والد زیاد و بیقاعده میشوند
اگر هر چیزی را Polymorphic کنید، سیستم مبهم و سختدیباگ میشود.
5. وقتی رابطه واقعاً عمومی نیست
Polymorphic برای موجودیتهای عمومی مناسب است؛ مثل کامنت، فایل، تگ و لاگ. اگر رابطه خاص یک دامنه است، رابطه معمولی ممکن است بهتر باشد.
خطاهای رایج در روابط Polymorphic
1. انتخاب نام مبهم برای رابطه
نامهایی مثل modelable یا relationable خوانایی کمی دارند. بهتر است نام رابطه معنا داشته باشد:
commentable
imageable
attachable
taggable
likeable
subject
2. استفاده نکردن از Morph Map
در پروژههای بلندمدت، ذخیره نام کامل کلاس در دیتابیس میتواند در آینده مشکلساز شود.
3. نداشتن Index مناسب
بدون Index روی *_type و *_id، Queryها در دادههای زیاد کند میشوند.
4. نادیده گرفتن N+1 Query
روابط Polymorphic نیز مثل سایر روابط Eloquent نیاز به Eager Loading دارند.
5. حذف والد بدون مدیریت فرزندها
چون Cascade Foreign Key ساده ندارید، باید حذف دادههای مرتبط را با Service، Observer یا Event مدیریت کنید.
6. ذخیره داده حساس در مدل عمومی
اگر جدول Polymorphic عمومی است، مراقب سطح دسترسی و دادههای حساس باشید.
7. استفاده بیش از حد از Polymorphic
هر رابطهای را نباید Polymorphic کرد. استفاده افراطی باعث مبهم شدن معماری میشود.
بهترین روشهای استفاده از Polymorphic در Laravel
1. فقط برای موجودیتهای واقعاً مشترک استفاده کنید
کامنت، فایل، تصویر، تگ، لاگ و لایک نمونههای مناسب هستند.
2. نام رابطه را دقیق انتخاب کنید
نام رابطه باید مفهوم تجاری داشته باشد؛ مثل commentable یا attachable.
3. از Morph Map استفاده کنید
برای جلوگیری از وابستگی دیتابیس به Namespace کلاسها، Morph Map تعریف کنید.
4. Index را جدی بگیرید
ستونهای *_type و *_id باید برای Queryهای پرتکرار بهینه شوند.
5. رفتار حذف را مشخص کنید
تصمیم بگیرید با حذف والد، فرزندها حذف شوند، Soft Delete شوند یا باقی بمانند.
6. API را از نام کلاس PHP جدا کنید
در خروجی API از نوعهای پایدار مثل post و product استفاده کنید، نه App\Models\Post.
7. Queryهای Polymorphic را تست کنید
بهخصوص Queryهایی که از whereHasMorph یا Eager Loading پیچیده استفاده میکنند.
8. منطق تجاری را در Service نگه دارید
Model فقط رابطه را تعریف کند؛ منطق پیچیده بهتر است در Service یا Action باشد.
9. مراقب Performance باشید
برای دادههای زیاد از Pagination، Eager Loading و Cache استفاده کنید.
10. مستندسازی کنید
در پروژههای تیمی، مشخص کنید هر جدول Polymorphic به چه مدلهایی میتواند متصل شود.
جدول مقایسه انواع Polymorphic در Laravel
| نوع رابطه | متد سمت والد | متد سمت فرزند/هدف | مثال | کاربرد |
|---|---|---|---|---|
| یکبهیک | morphOne | morphTo | تصویر اصلی محصول یا کاربر | Avatar، تصویر شاخص |
| یکبهچند | morphMany | morphTo | کامنتهای مقاله و محصول | کامنت، فایل، لاگ |
| چندبهچند | morphToMany | morphedByMany | تگهای مقاله و ویدئو | تگ، دستهبندی مشترک |
| یکی از چندتا | morphOne + latestOfMany | morphTo | آخرین تصویر محصول | رکورد منتخب از چند رکورد |
| رابطه معکوس | ندارد | morphTo | والد کامنت | یافتن مدل مالک |
نمونه کامل: پیادهسازی کامنت Polymorphic در Laravel
Migration
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->text('body');
$table->morphs('commentable');
$table->timestamps();
});
Comment Model
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Comment extends Model
{
protected $fillable = [
'user_id',
'body',
];
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
Post Model
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Post extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
Product Model
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Product extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
Store Comment
$post = Post::findOrFail($postId);
$post->comments()->create([
'user_id' => auth()->id(),
'body' => request('body'),
]);
Read Comments
$post = Post::with('comments')->findOrFail($postId);
return $post->comments;
Read Parent
$comment = Comment::with('commentable')->findOrFail($commentId);
return $comment->commentable;
Polymorphic و سئو در پروژههای Laravel
Polymorphic بهصورت مستقیم ابزار SEO نیست، اما در پروژههای محتوایی و فروشگاهی میتواند به طراحی تمیزتر ساختار محتوا کمک کند. برای مثال:
- تگها میتوانند بین مقالهها، ویدئوها و محصولات مشترک باشند.
- کامنتها میتوانند برای انواع محتوا یکپارچه مدیریت شوند.
- تصاویر و فایلهای رسانهای میتوانند ساختار مشترک داشته باشند.
- Activity Log انتشار محتوا میتواند برای مدلهای مختلف ثبت شود.
- Sitemap یا Cache میتواند با Eventهای مرتبط با مدلهای Polymorphic بهروزرسانی شود.
مثلاً اگر Tag بهصورت Polymorphic برای مقاله و ویدئو استفاده شود، میتوانید صفحات آرشیو موضوعی قویتری طراحی کنید. البته باید مراقب باشید ساختار URL، Canonical و دادههای متا برای هر نوع محتوا جداگانه و دقیق مدیریت شود.
FAQ؛ سوالات متداول درباره Polymorphic در Laravel
1. Polymorphic در Laravel چیست؟
Polymorphic Relationship در Laravel نوعی رابطه Eloquent است که اجازه میدهد یک مدل به چند نوع مدل مختلف متصل شود. مثلاً یک کامنت میتواند متعلق به مقاله، محصول یا ویدئو باشد.
2. ستونهای commentable_id و commentable_type چه کاربردی دارند؟
commentable_id شناسه مدل والد را نگه میدارد و commentable_type نوع مدل والد را مشخص میکند. این دو ستون با هم تعیین میکنند رکورد مربوطه به کدام مدل و کدام رکورد متصل است.
3. تفاوت morphOne و morphMany چیست؟
morphOne برای رابطه یکبهیک Polymorphic استفاده میشود؛ مثل یک تصویر اصلی برای محصول. morphMany برای رابطه یکبهچند استفاده میشود؛ مثل چند کامنت برای مقاله.
4. morphTo چه کاربردی دارد؟
morphTo در مدل فرزند تعریف میشود و رابطه معکوس را نشان میدهد. مثلاً در مدل Comment، متد commentable() با morphTo مشخص میکند کامنت به کدام مدل والد تعلق دارد.
5. morphToMany چیست؟
morphToMany برای رابطه چندبهچند Polymorphic استفاده میشود. مثال رایج آن سیستم تگ است؛ جایی که مقالهها و ویدئوها میتوانند تگهای مشترک داشته باشند.
6. morphedByMany چه تفاوتی با morphToMany دارد؟
morphToMany در مدلهایی تعریف میشود که به مدل مشترک وصل میشوند؛ مثل Post و Video. morphedByMany در مدل مشترک تعریف میشود؛ مثل Tag.
7. آیا در Polymorphic میتوان Foreign Key تعریف کرد؟
بهصورت معمول و ساده خیر؛ چون یک ستون مثل commentable_id ممکن است به جدولهای مختلف اشاره کند. بنابراین یکپارچگی داده باید با منطق برنامه، تست، Service و سیاست حذف مناسب مدیریت شود.
8. Morph Map چیست؟
Morph Map اجازه میدهد بهجای ذخیره نام کامل کلاس مثل App\Models\Post در دیتابیس، مقدار کوتاه و پایدار مثل post ذخیره شود. این کار برای پروژههای حرفهای توصیه میشود.
9. آیا Polymorphic باعث کندی میشود؟
خود Polymorphic الزاماً کند نیست، اما اگر Index مناسب، Eager Loading و Query بهینه نداشته باشید، در دادههای زیاد میتواند مشکل Performance ایجاد کند.
10. چه زمانی نباید از Polymorphic استفاده کنیم؟
اگر رابطه نیاز به Foreign Key سخت دیتابیس دارد، منطق مدلهای مختلف کاملاً متفاوت است یا Queryهای تحلیلی پیچیده دارید، باید با احتیاط از Polymorphic استفاده کنید و شاید رابطه معمولی بهتر باشد.
11. آیا Polymorphic برای سیستم فایل مناسب است؟
بله. فایلها و ضمیمهها یکی از بهترین سناریوهای استفاده از Polymorphic هستند، چون ممکن است به مدلهای مختلفی مثل تیکت، فاکتور، قرارداد و کاربر متصل شوند.
12. آیا Polymorphic برای سیستم تگ مناسب است؟
بله. برای تگهایی که روی چند نوع محتوا مثل مقاله، ویدئو و محصول استفاده میشوند، رابطه چندبهچند Polymorphic انتخاب رایجی است.
جمعبندی
Polymorphic در Laravel یکی از قابلیتهای قدرتمند Eloquent برای طراحی رابطههای انعطافپذیر و قابل توسعه است. با استفاده از روابط چندشکلی میتوان مدلهایی مثل کامنت، فایل، تصویر، لاگ، لایک، تگ و امتیازدهی را به چند نوع مدل مختلف متصل کرد، بدون اینکه برای هر مدل جدول و منطق جداگانه بسازیم.
این قابلیت در پروژههای شرکتی، فروشگاهی، محتوایی، CRM، ERP و سیستمهای SaaS بسیار کاربردی است؛ زیرا بسیاری از ماژولهای عمومی باید در بخشهای مختلف سیستم استفاده شوند. بهجای تکرار جدولها و کدها، Polymorphic کمک میکند ساختاری متمرکزتر و قابل نگهداریتر داشته باشیم.
با این حال، Polymorphic باید هدفمند استفاده شود. نبود Foreign Key مستقیم، نیاز به Index مناسب، احتمال N+1 Query، پیچیدگی در گزارشگیری و مدیریت حذف دادهها از نکاتی هستند که باید جدی گرفته شوند. برای پروژههای حرفهای، استفاده از Morph Map، Eager Loading، Service Layer، تستنویسی و مستندسازی رابطهها بسیار توصیه میشود.
اگر درست طراحی شود، Polymorphic Relationship میتواند یکی از بهترین ابزارهای Laravel برای ساخت ماژولهای مشترک، تمیز و توسعهپذیر باشد. 🧬
CTA
اگر در پروژه Laravel خود با ماژولهایی مثل کامنت، فایل پیوست، تگ، لاگ فعالیت، لایک یا تصاویر مشترک سروکار دارید، طراحی درست روابط Polymorphic میتواند ساختار دیتابیس و کد شما را بسیار تمیزتر کند. تیم ما میتواند در طراحی معماری Eloquent، بازطراحی دیتابیس، پیادهسازی روابط چندشکلی، بهینهسازی Queryها، کاهش N+1، طراحی API و استانداردسازی پروژه Laravel به شما کمک کند. برای بررسی فنی پروژه خود با ما تماس بگیرید و ساختار نرمافزار خود را حرفهایتر و توسعهپذیرتر کنید. 🚀
منابع رسمی
- مستندات رسمی Laravel درباره Eloquent Relationships؛ برای مطالعه انواع رابطههای Eloquent، از جمله morphOne، morphMany، morphTo، morphToMany و morphedByMany.
مطالعه مستندات رسمی Laravel درباره روابط Eloquent - مستندات رسمی Laravel درباره Migrations؛ برای آشنایی با Schema Builder و متدهای مربوط به ساخت ستونهای رابطهای و Morph در Migrationها.
مطالعه مستندات رسمی Laravel درباره Migrations