لنفترض أن لدينا الجداول التالية في المشروع:

articles
videos
books

ولكل جدول من هذه الجداول مجموعة من التعليقات الخاصة به.

لتخزين هذه التعليقات، يلجأ البعض لإنشاء جدول تعليقات منفصل لكل نوع، مثلًا:

article_comments
    id
    comment
    article_id
    timestamps

video_comments
    id
    comment
    video_id
    timestamps

book_comments
    id
    comment
    book_id
    timestamps

كما نلاحظ، جميع الحقول متشابهة في هذه الجداول الثلاثة، باستثناء حقل الربط (article_id، video_id، book_id). هذا التكرار في البنية هو بالضبط المشكلة التي تحلها علاقة Polymorphic Relationship في Laravel.


ما هي علاقة Polymorphic

توفر Laravel علاقة Polymorphic لحل هذه المشكلة، حيث يتم إنشاء جدول واحد فقط لجميع التعليقات، بغض النظر عن نوع الجدول الذي تنتمي إليه (مقال، فيديو، كتاب...). يكون شكل الجدول كالتالي:

comments
    id
    comment
    commentable_id
    commentable_type
    timestamps

بدلًا من ثلاثة جداول منفصلة، أصبح لدينا جدول واحد يخدم جميع الأنواع، ويميّز بين السجلات عبر حقلين إضافيين فقط: commentable_id وcommentable_type.


إنشاء جدول comments

يمكن إنشاء الموديل والـ migration معًا عبر الأمر التالي:

php artisan make:model Comment -m

ويجب أن يحتوي ملف الـ migration على الحقول التالية:

public function up()
{
    Schema::create('comments', function (Blueprint $table) {
        $table->id();
        $table->string('comment');
        $table->morphs('commentable');
        $table->timestamps();
    });
}

توضيح

  • يوجد هنا نوع جديد من الحقول وهو morphs، والذي يقوم تلقائيًا بإنشاء حقلين: commentable_id وcommentable_type في جدول comments.
  • اسم commentable هنا مرتبط باسم جدول comments؛ فلو كان اسم الجدول photos مثلًا، لوجب أن يكون اسم النوع الممرر لـ morphs هو photoable، وليس commentable. بمعنى آخر: يُؤخذ اسم الجدول بصيغة singular (مفرد)، ثم تُضاف له able.
  • حقل commentable_id: يُخزَّن فيه id السجل التابع له التعليق (id المقال، أو الفيديو، أو الكتاب).
  • حقل commentable_type: يُخزَّن فيه مسار الـ Model التابع له التعليق (مثل App\Models\Book، App\Models\Article...).

مثال على شكل البيانات المخزنة

لتوضيح الفكرة أكثر، لو كان لدينا عدة تعليقات على كتب ومقالات مختلفة، فسيكون شكل جدول comments تقريبًا كالتالي:

idcommentcommentable_idcommentable_type
1Book 15App\Models\Book
2Book 27App\Models\Book
3Article 15App\Models\Article

ملاحظتان مهمتان على الجدول أعلاه

  1. التعليقان Book 1 وBook 2 يتبعان لموديل Book، وقد عرفنا ذلك من خلال قيمة commentable_type.
  2. لاحظ أن قيمة commentable_id قد تتكرر بين سجلات مختلفة تمامًا؛ فالتعليق على الكتاب رقم 5 والتعليق على المقال رقم 5 كلاهما يحملان نفس commentable_id (وهو 5)، لكن يتم التمييز بينهما بدقة عبر حقل commentable_type. فبدون هذا الحقل، كان سيستحيل معرفة أن id رقم 5 يخص كتابًا في سجل، ومقالًا في سجل آخر.

إنشاء العلاقات في الموديلات

الموديل Comment

في موديل Comment، نضيف دالة باسم commentable (وقد أوضحنا سابقًا سبب هذه التسمية تحديدًا):

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

نوع العلاقة هنا هو morphTo، وهي العلاقة العكسية التي تسمح للتعليق بمعرفة أي موديل ينتمي إليه، دون الحاجة لتحديد اسم الموديل صراحة (لأنه مخزَّن أصلًا في commentable_type).

الموديلات الأخرى: Book، Article، Video

في كل موديل من هذه الموديلات، نضيف دالة باسم comments، تستخدم علاقة morphMany، مع تحديد اسم موديل التعليق واسم العلاقة (commentable):

public function comments(): MorphMany
{
    return $this->morphMany(Comment::class, 'commentable');
}

يجب تكرار هذه الدالة في جميع الموديلات التي نريد لها القدرة على استقبال تعليقات (Book، Article، Video).


إضافة التعليقات (إدخال البيانات في علاقة Polymorphic)

عند إضافة تعليق، نستخدم العلاقة comments المعرَّفة في الموديل، بدلاً من التعامل مباشرة مع Comment Model. مثال على إضافة تعليق على كتاب معين:

public function addComment($book_id, Request $request)
{
    $book = Book::findOrFail($book_id);

    $book->comments()->create([
        'comment' => $request->comment
    ]);
}

مع ملاحظة أنه يجب إضافة الحقل comment ضمن fillable في موديل Comment، حتى تعمل عملية الإنشاء الجماعي (Mass Assignment) بدون أخطاء:

protected $fillable = ['comment'];

وبنفس المنطق، لإضافة تعليق على مقال:

public function addComment($article_id, Request $request)
{
    $article = Article::findOrFail($article_id);

    $article->comments()->create([
        'comment' => $request->comment
    ]);
}

الكود متطابق تقريبًا في الحالتين، والفرق الوحيد هو الموديل المستخدم؛ Laravel يتكفل تلقائيًا بملء حقلي commentable_id وcommentable_type بالقيم الصحيحة عند استدعاء create() عبر العلاقة.


عرض التعليقات (عرض البيانات في علاقة Polymorphic)

لعرض التعليقات الخاصة بمقال معين، نستخدم with مع اسم العلاقة comments:

$article = Article::with('comments')->where('id', $article->id)->first();

وبنفس الطريقة تمامًا، يمكن عرض تعليقات أي كتاب أو فيديو، طالما تم تعريف علاقة comments في الموديل الخاص به.


جدول ملخّص للمفاهيم

العنصرالوصف
morphs('commentable')ينشئ حقلي commentable_id وcommentable_type في جدول comments
morphTo()يُستخدم في موديل Comment للوصول إلى الموديل التابع له التعليق (عكسي)
morphMany()يُستخدم في الموديلات الأخرى (Book, Article, Video) للوصول إلى كل التعليقات الخاصة بها
commentable_typeيحدد مسار الموديل (namespace) التابع له التعليق
commentable_idيحدد id السجل التابع له التعليق

الخلاصة

توفّر علاقة Polymorphic في Laravel حلًا أنيقًا لمشكلة تكرار بنية الجداول عند الحاجة لربط نموذج واحد (مثل التعليقات) بعدة نماذج مختلفة (مقالات، فيديوهات، كتب...). بدلًا من إنشاء جدول منفصل لكل نوع، نكتفي بجدول واحد يحتوي على حقلي commentable_id وcommentable_type، عبر:

  • استخدام morphs() في الـ migration لإنشاء الحقول اللازمة تلقائيًا.
  • استخدام morphTo() في الموديل المركزي (Comment).
  • استخدام morphMany() في باقي الموديلات (Book، Article، Video).

هذا النمط يقلل التكرار في قاعدة البيانات، ويجعل إضافة نموذج جديد يدعم التعليقات مسألة سطر واحد فقط من الكود، دون الحاجة لإنشاء جدول أو migration جديد في كل مرة.