لنفترض أن لدينا الجداول التالية في المشروع:
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 تقريبًا كالتالي:
| id | comment | commentable_id | commentable_type |
|---|---|---|---|
| 1 | Book 1 | 5 | App\Models\Book |
| 2 | Book 2 | 7 | App\Models\Book |
| 3 | Article 1 | 5 | App\Models\Article |
ملاحظتان مهمتان على الجدول أعلاه
- التعليقان Book 1 وBook 2 يتبعان لموديل Book، وقد عرفنا ذلك من خلال قيمة commentable_type.
- لاحظ أن قيمة 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 جديد في كل مرة.
السلام عليكم بارك الله فيك على هذا الشرح الممتاز , جعله الله بموازين حسناتك لكن بارك الله فيك عندي ملاحظات وهي هامة ماني عارف كيف أوصل لك الفكر , لاحظت أثناء شرحك أنك تخرج عن المحتوى الأساسي , مثلاً السطر هذا : (( ويتم به تخزين الصور لجميع الحقول الأخرى (articles, videos, users). )) الموجود بنفس الموضوع هذا ماعلاقة الصور بالتعليقات وأنت بالأساس الموضوع كله ما يتكلم عن الصور وانما حول التعليقات وأيضا مواضيع أخرى فيها انحراف عن سياق الحديث أو الشرح ان جاز التعبير طبعاً المبتدئ تهمة الدقة لأنه يقرأ بتمعن ويتبع الخطوات بحذافيرها حتى توصله المعلومة بالشكل المطلوب بينما المبتدأ راح يجلس يدور حوالين نفسه ماهو فاهم تداخل بعض الأمور في بعضها وأنا وجدت بصراحة عدة مواضيع , فياليت كرما منك لو تعدل الكلمات بحيث تكون ماشية مع الشرح نفسه حتى ما يصير لخبطه وما يستفيد الانسان من هذا الجهد الكبير مشكوراً عليه من أعماق قلبي المعذرة ان كان اسلوبي فيه شيء مزعج وشكرا لك
وعليكم السلام ورحمة الله وبركاته، وبارك الله فيك على هذه الملاحظة الدقيقة والصريحة، وأسلوبك لم يكن مزعجًا إطلاقًا، بل بالعكس، هذا بالضبط النوع من الملاحظات المفيد.
أنت محق تمامًا. جملة "ويتم به تخزين الصور لجميع الحقول الأخرى" هي فعلًا خطأ متبقٍّ (على الأرجح من صياغة مقال آخر يتحدث عن جدول صور Polymorphic)، ولا علاقة لها بموضوع هذا المقال الذي يتحدث حصرًا عن التعليقات. هذا النوع من الأخطاء يحدث أحيانًا عند إعادة استخدام هيكل شرح مشابه لموضوع مختلف، وكان يجب مراجعته بدقة أكبر قبل النشر.
شكرًا جزيلًا لتنبيهك، تم التصحيح. جزاك الله خيرًا على وقتك وحرصك على إفادة الآخرين.
مشكور كتير مهندس ايثار على الشرح، استمر والي الأمام. عندي استفسار على هذه النقطة: $article=Article::with(\'comments\')->where(\'id\',$article->id)->first() ماذا لو أردنا البحث في الجدول ال comment المرتبط بال article؟
شكرًا جزيلًا لك، وأهلًا بك دائمًا. سؤال ممتاز، والإجابة تختلف قليلًا حسب الهدف بالضبط:
الحالة الأولى: إذا أردنا البحث ضمن تعليقات مقال معيّن نملكه بالفعل (بمعنى: عندي article وأريد فلترة تعليقاته):
$comments = $article->comments() ->where('comment', 'like', '%كلمة البحث%') ->get();الحالة الثانية: إذا أردنا البحث عن المقالات التي تحتوي على تعليق معين (بمعنى: الفلترة تبدأ من جهة Article وليس من تعليق موجود مسبقًا)، نستخدم whereHas:
$articles = Article::whereHas('comments', function ($query) { $query->where('comment', 'like', '%كلمة البحث%'); })->get();الفرق بينهما: الطريقة الأولى تبحث داخل تعليقات مقال واحد محدد مسبقًا، بينما whereHas تبحث في جدول articles نفسه وتُرجع فقط المقالات التي لديها تعليق مطابق للشرط. حسب ما تحتاجه بالضبط في الاستفسار، إحدى الطريقتين ستناسبك.
كلام زيي الورد
شكرًا جزيلًا لك على كلامك الجميل!