العلاقات في رابط الكائنات بالعلاقات Eloquent

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

مقدمة

في معظم الأحيان، تكون جداول قواعد البيانات مرتبطة ببعضها البعض. على سبيل المثال، قد يكون لمقالة ما الكثير من التعليقات، أو قد تكون الطلبيّة مرتبطة بالمستخدم الذي قام بها. يجعل Eloquent العمل مع هذه العلاقات أمرًا سهلًا، ويدعم عدة أنواع من العلاقات المختلفة:

  • واحد إلى واحد (One To One)
  • واحد إلى كثير (One To Many)
  • كثير إلى كثير (Many To Many)
  • الكثير ضمن (Has Many Through)
  • العلاقات متعددة الأشكال (Polymorphic Relationships)
  • علاقات الكثير إلى كثير متعددة الأشكال (Many To Many Polymorphic Relationships)

تعريف العلاقات

تعرّف العلاقات كتوابع في أصناف نماذج Eloquent. وبما أنّ العلاقات تلعب دور منشئ الاستعلامات (كما هو الحال في النماذج نفسها)، فإنّ تعريف العلاقات كتوابع يسهّل سلسلة التوابع ويزوّد بإمكانات الاستعلام. على سبيل المثال، يمكننا أن نسلسل قيودًا إضافية على العلاقة posts التالية:

$user->posts()->where('active', 1)->get();

لكن قبل المضي قدمًا بكيفية استخدام العلاقات، لنلقِ نظرة على كيفية تعريف كل نوع من العلاقات.

واحد إلى واحد One to One

إن علاقة الواحد إلى واحد هي علاقة بسيطة ججدًّا وأساسية. على سبيل المثال، قد يكون نموذج المستخدم User مرتبطًا بنموذج هاتف Phone وحيد. لتعريف هذه العلاقة، نعرف التابع phone في النموذج User. يجب على التابع phone أن يستدعي التابع hasOne ويعيد نتيجته:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
   /**
    * قراءة سجل الهاتف المرتبط بالمستخدم.
    */
   public function phone()
   {
       return $this->hasOne('App\Phone');
   }
}

الوسيط الأول الممرر للتابع hasOne هو اسم النموذج المرتبط. بعد تعريف العلاقة، يمكننا استرداد السجل المرتبط باستخدام الخاصّيات الديناميكية لـ Eloquent. تمكّنك الخاصيات الديناميكية من الوصول إلى توابع العلاقات وكأنّها خاصيات معرفة في النموذج:

$phone = User::find(1)->phone;

يحدد Eloquent المفتاح الأجنبي للعلاقة بناءً على اسم النموذج. في هذه الحالة، يُعتقد أن يملك النموذج Phone على حقل المفتاح الأجنبي user_id. إذا أردت أن تعدّل هذا العرف، يمكنك تمرير وسيط ثاني للتابع hasOne:

return $this->hasOne('App\Phone', 'foreign_key');

علاوةً على ذلك، يعتقد Eloquent أن المفتاح الأجنبي يجب أن يقابله الحقل id (أو الـ primaryKey المعدل) من الأب. مما يعني أنّ Eloquent سيبحث عن القيمة id الخاصة بالمستخدم في الحقل user_id الخاص بالنموذج Phone. في حال أردت من العلاقة أن تنظر في قيمة مختلفة عن id، يمكنك تمرير وسيط ثالث للتابع hasOne يحدد المفتاح المعدل:

return $this->hasOne('App\Phone', 'foreign_key', 'local_key');

تعريف معكوس العلاقة

الآن يمكننا الوصول للسجل Phone من النموذج User. لنعرّف الآن العلاقة على النموذج Phone التي تمكننا من الوصول للمستخدم User الذي يملك الهاتف. يمكننا تعريف معكوس العلاقة hasOne عن طريق التابع belongsTo:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Phone extends Model
{
   /**
    * قراءة المستخدم الذي يملك الهاتف.
    */
   public function user()
   {
       return $this->belongsTo('App\User');
   }
}

في المثال أعلاه، سيحاول Eloquent أن تقارن الحقل user_id من النموذج Phone بالحقل id من النموذج User. يحدد Eloquent الاسم الافتراضي للمفتاح الرئيسي بالنظر إلى اسم العلاقة وجمعها مع "id_"، لكن في حال كان المفتاح الأجنبي على النموذج Phone ليس user_id، يمكنك تمرير الاسم المختلف بالوسيط الثاني للتابع belongsTo:

public function user()
{
   return $this->belongsTo('App\User', 'foreign_key');
}

في حال كان النموذج الأب لا يملك الحقل id كالمفتاح الرئيسي له، أو أردت دمج النموذج الإبن إلى حقل ثاني، يمكنك تمرير وسيط ثالث للتابع belongsTo يحدد المفتاح الرئيسي المعدل للجدول الأب:

public function user()
{
   return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}

واحد إلى كثير One to Many

تمكّنك العلاقة "واحد إلى كثير" من تعريف العلاقات التي يكون السجل الواحد فيها مرتبطًا بالعديد من السجلات الأخرى من النماذج الأخرى.مثلًا، قد تملك المقالة عددًا لا نهائيًّا من التعليقات. كما هو الحال مع كل علاقات Eloquent الأخرى، يمكن تعريف العلاقة واحد إلى كثير بتعريف تابع في نموذج Eloquent:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
   /**
    * قراءة التعليقات الخاصة بالمقالة.
    */
   public function comments()
   {
       return $this->hasMany('App\Comment');
   }
}

تذكر، يحدد Eloquent المفتاح الأجنبي المناسب تلقائيًّا على النموذج Comment. كعرف متخذ، يأخذ Eloquent اسم النموذج الأب في صيغة snake case ويجمعه مع "id_". لذا في هذا المثال، يعتقد Eloquent أن المفتاح الأجنبي في النموذج Comment هو post_id. بعد تعريف العلاقة، يمكننا الوصول لمجموعة التعليقات المرتبطة بالمقالة عن طريق الوصول للخاصية comments. تذكر، بما أن Eloquent يزودنا بالخاصيات الديناميكية، يمكننا الوصول إلى توابع العلاقات وكأنها خاصيات:

$comments = App\Post::find(1)->comments;

foreach ($comments as $comment) {

  //

}

وبالطبع، بما أن جميع العلاقات تلعب دور منشئ الاستعلامات، يمكننا إضافة قيود إضافية إلى التعليقات المستردة من التابع comments عن طريق سلسلة الشروط على الاستعلام:

$comment = App\Post::find(1)->comments()->where('title', 'foo')->first();

كما هو الحال بالتابع hasOne، يمكنك تعديل عرف المفاتيح الأجنبية والرئيسية عن طريق تمرير الوسيطين الإضافيين إلى التابع hasMany:

return $this->hasMany('App\Comment', 'foreign_key');

return $this->hasMany('App\Comment', 'foreign_key', 'local_key');

تعريف معكوس العلاقة

يمكننا الآن الوصول للتعليقات المتعلقة بالمقالة. لنعرّف الآن العلاقة التي تمكننا من الوصول للمقالة المرتبطة بالتعليق. لتعريف معكوس العلاقة hasMany، عرّف تابعًا في النموذج الإبن يستدعي التابع belongsTo:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
   /**
    * قراءة المقالة التي تملك التعليق.
    */
   public function post()
   {
       return $this->belongsTo('App\Post');
   }
}

بعد تعريف العلاقة، يمكننا الوصول للنموذج Post المتعلق بالنموذج Comment عن طريق الوصول للخاصية الديناميكية post:

$comment = App\Comment::find(1);

echo $comment->post->title;

في المثال أعلاه، يحاول Eloquent مطابقة الحقل post_id من النموذج Comment بالحقل id من النموذج Post. يحدد Eloquent الاسم الافتراضي للمفتاح الأجنبي بالنظر إلى اسم تابع العلاقة وجمعه بـ "_" مسبوقة باسم المفتاح الرئيسي. لكن في حال كان المفتاح الأجنبي في النموذج Comment ليس post_id، يمكنك تمرير الاسم المعدل للمفتاح الأجنبي بالوسيط الثاني للتابع belongsTo:

public function post()
{
   return $this->belongsTo('App\Post', 'foreign_key');
}

إذا لم يكن النموذج الأب يملك حقل id كالمفتاح الرئيسي له، أو في حال أردت دمج النموذج الابن بحقل مختلف، يمكنك تمرير وسيط ثالث للتابع belongsTo يحدد المفتاح الرئيسي المعدل للجدول الاب:

public function post()
{
   return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
}

كثير إلى كثير Many to Many

إن العلاقات "كثير إلى كثير" أكثر تعقيدًا من العلاقات hasOne و hasMany. أبسط مثال على هذه العلاقة هو المستخدم الذي يملك عدة أدوار، والدور المشترك من أكثر من مستخدم. مثلًا، قد يملك العديد من المستخدمين الدور "Admin". لتعريف هذه العلاقة، يجب وجود ثلاث جداول في قاعدة المعطيات: users و roles و role_user. إن الجدول role_user (المدعو بجدول الكسر) مشتق من الترتيب الأبجدي لأسماء النماذج المرتبطة، ويحتوي على الحقلين user_id و role_id.

يمكن تعريف العلاقات كثير إلى كثير عن طريق تعريف تابع يعيد نتيجة التابع belongsToMany. مثلًا، لنعرّف التابع roles على نموذج المستخدم User:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
   /**
    * الأدوار المرتبطة بالمستخدم.
    */
   public function roles()
   {
       return $this->belongsToMany('App\Role');
   }
}

بعد تعريف العلاقة، يمكن الوصول إلى أدوار المستخدم عن طريق الخاصية الديناميكية roles:

$user = App\User::find(1);

foreach ($user->roles as $role) {

  //

}

وبالطبع كما هو الحال مع جميع أنواع العلاقات، يمكنك استدعاء التابع roles وسلسلة شروط الاستعلام على العلاقة كالتالي:

$roles = App\User::find(1)->roles()->orderBy('name')->get();

وكما ذُكر سابقًا، لتحديد اسم الجدول المستخدم لكسر العلاقة، يدمج Eloquent أسماء النموذجين بترتيب أبجدي. لكن يمكنك تعديل ذلك العرف عن طريق تمرير الوسيط الثاني للتابع belongsToMany:

return $this->belongsToMany('App\Role', 'role_user');

إضافةً إلى تعديل اسم جدول الكسر، يمكنك تعديل أسماء حقول المفاتيح الأجنبية في الجدول عن طريق تمرير الوسائط الإضافية للتابع belongsToMany. يمثل الوسيط الثالث اسم المفتاح الأجنبي للنموذج الذي تعرف العلاقة عليه، بينما يمثل الوسيط الرابع اسم المفتاح الأجنبي للنموذج الذي تدمج فيه:

return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');

تعريف معكوس العلاقة

لتعريف معكوس علاقة الكثير إلى كثير، يمكنك إنشاء استدعاء مماثل للتابع belongsToMany على النموذج المرتبط. بالاستكمال مع مثال الأدوار، لنعرّف التابع users على النموذج Role:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
   /**
    * المستخدمون الذين ينتمون للدور.
    */
   public function users()
   {
       return $this->belongsToMany('App\User');
   }
}

نلاحظ أن العلاقة معرّفة تمامًا مثلما عُرّفت في النموذج User، باستثناء تمرير النموذج App\User. بما أننا نعيد استعمال التابع belongsToMany، جميع تعديلات المفاتيح الأجنبية وأسماء الجداول تكون متاحة عند تعريف معكوس علاقة الكثير إلى كثير.

استرداد حقول الجداول الوسيطيّة

كما تعلّمت مسبقًا، فإن العمل مع علاقات الكثير إلى الكثير يتطلّب وجود جدول كسر أو جدول وسيطي. يزوّد Eloquent بطريقة سهلة للتعامل مع هذا الجدول. على سبيل المثال، لنفرض أن كائن المستخدم User يمتلك العديد من كائنات الأدوار Role المرتبطة به. بعد الوصول لهذه العلاقة، يمكننا الوصول للجدول الوسيطي عن طريق الخاصية pivot على النماذج:

$user = App\User::find(1);

foreach ($user->roles as $role) {
   echo $role->pivot->created_at;
}

لاحظ أن كل سجل Role نحصل عليه يمتلك خاصية pivot. هذه الخاصية تحتوي على نموذج يمثل الجدول الوسيطي، ويمكن استخدامه مثل أي نموذج Eloquent آخر. افتراضيًّا، سيحتوي الكائن pivot فقط على مفاتيح النماذج. في حال كان الجدول الوسيطي يمتلك خاصيات إضافية، يجب أن تحددها عند تعريف العلاقة:

return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');

وفي حال أردت أن يحتوي الجدول الوسيطي على الحقول الزمنية created_at و updated_at، استخدم التابع withTimestamps على تعريف العلاقة:

return $this->belongsToMany('App\Role')->withTimestamps();

تخصيص اسم الخاصية pivot

كما ذُكر آنفًا، يمكن الوصول للجدول الوسيطي من خلال الخاصية pivot. لكن يمكنك تخصيص اسم هذه الخاصية لعكس المغزى منها بشكل أفضل ضمن تطبيقك.

على سبيل المثال، قد يمكّن تطبيقك من المستخدمين أن يقوموا بالتسجيل بالتدوينات الصوتية (podcasts)، قد تملك علاقة كثير إلى كثير بين المستخدمين والتدوينات الصوتية. إن كانت هذه الحالة، قد تحتاج إلى تسمية كائن الجدول الوسيطي بـ subscription بدلًا من pivot. يمكن تحقيق ذلك من خلال التابع as المستخدم عند تعريف العلاقة:

return $this->belongsToMany('App\Podcast')
               ->as('subscription')
               ->withTimestamps();

بعد ذلك، يمكنك الوصول لبيانات الجدول الوسيطي باستخدام الاسم المخصص:

$users = User::with('podcasts')->get();

foreach ($users->flatMap->podcasts as $podcast) {
   echo $podcast->subscription->created_at;
}

ترشيح العلاقات باستخدام حقول الجداول الوسيطية

يمكنك ترشيح نتائج استعلام العلاقة belongsTo عن طريق التوابع wherePivot و wherePivotIn وذلك عند تعريف العلاقة:

return $this->belongsToMany('App\Role')->wherePivot('approved', 1);

return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);

تعريف نماذج مخصّصة للجداول الوسيطية

في حال أردت أن تعرّف نماذج مخصصة لتمثيل الجدول الوسيطي في علاقتك، يمكنك استخدام التابع using عند تعريف العلاقة. يجب على نماذج الجداول الوسيطية أن ترث من الصنف Illuminate\Database\Eloquent\Relations\Pivot، بينما يجب على نماذج الجداول الوسيطية للعلاقات متعددة الأشكال أن ترث من الصنف Illuminate\Database\Eloquent\Relations\MorphPivot. مثلًا، يمكننا تعريف علاقة الأدوار باستخدام النموذج الوسيطي UserRole:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
   /**
    * المستخدمون الذين ينتمون للدور.
    */
   public function users()
   {
       return $this->belongsToMany('App\User')->using('App\UserRole');
   }
}

عند تعريف النموذج UserRole، سنرث من الصنف Pivot:

<?php

namespace App;

use Illuminate\Database\Eloquent\Relations\Pivot;

class UserRole extends Pivot
{

  //

}

الكثير ضمن (Has Many Through)

تمكّنك علاقة "الكثير ضمن" من الوصول بشكل سريع للعلاقات المتباعدة والتي يفصل بينها علاقات وسيطية. مثلًا، قد يملك نموذج البلد Country العديد من المنشورات Post ضمن النموذج الوسيطي User. في هذا المثال، يمكنك الوصول للمنشورات الموافقة للبلد المحدد بسهولة. لنلقِ نظرة على الجداول التي تشكّل هذه العلاقة:

countries
   id - integer
   name - string


users
   id - integer
   country_id - integer
   name - string


posts
   id - integer
   user_id - integer
   title - string

بالرغم من أن الجدول posts لا يمتلك حقل country_id، تمكّنك العلاقة hasManyThrough من الوصول إلى منشورات البلد من خلال ‎$country->posts. لتنفيذ هذا الاستعلام، ينظر Eloquent إلى الحقل country_id على الجدول users. بعد العثور على المفاتيح المطابقة، تستخدم هذه المفاتيح للاستعلام في الجدول posts. والآن بما أننا اطّلعنا على هيكلية الجداول المشكّلة للعلاقة، يمكننا تعريف العلاقة على النموذج Country:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Country extends Model
{
   /**
    * قراءة منشورات البلد.
    */
   public function posts()
   {
       return $this->hasManyThrough('App\Post', 'App\User');
   }
}

يحدد أول وسيط ممرر للتابع hasManyThrough اسم النموذج الأخير المراد الوصول إليه، بينما يحدد الوسيط الثاني اسم النموذج الوسيط. ستُستخدم أعراف Eloquent التقليدية للتعامل مع المفاتيح الأجنبية في هذه الحالة. في حال أردت تعديل أسماء المفاتيح الأجنبية لهذه العلاقة، يمكنك تمرير وسائط افتراضية للتابع hasManyThrough. يمثل الوسيط الثالث اسم المفتاح الأجنبي على النموذج الوسيطي، بينما يمثل الوسيط الرابع اسم المفتاح الأجنبي على النموذج الأخير المراد الوصول إليه. يمثل الوسيط الخامس اسم المفتاح الرئيسي للنموذج الأساسي، بينما يمثل الوسيط السادس اسم المفتاح الرئيسي للنموذج الوسيطي:

class Country extends Model
{
   public function posts()
   {
       return $this->hasManyThrough(
           'App\Post',
           'App\User',
           'country_id', // المفتاح الأجنبي على جدول المستخدمين users..
           'user_id', // المفتاح الأجنبي على جدول المنشورات posts..
           'id', // المفتاح الرئيسي على جدول البلدان countries..
           'id' // المفتاح الرئيسي على جدول المستخدمين users..
       );
   }
}

العلاقات متعددة الأشكال (Polymorphic Relationships)

هيكلية الجداول

تمكّنك العلاقات متعددة الأشكال من ربط نموذج وحيد بأكثر من نموذج آخر على علاقة واحدة. على سبيل المثال، تخيّل أن مستخدمي تطبيقك يمكنهم التعليق (comment) على المنشورات (posts) والفيديوهات (videos). باستخدام العلاقات متعددة الأشكال، يمكنك استخدام جدول comments وحيد للعلاقتين الاثنين. لنطّلع أولًا على هيكلية الجداول المطلوبة لتنفيذ العلاقة:

posts
   id - integer
   title - string
   body - text


videos
   id - integer
   title - string
   url - string


comments
   id - integer
   body - text
   commentable_id - integer
   commentable_type - string

تجدر الملاحظة أن هناك حقلين أساسيين على جدول التعليقات comments وهما commentable_id و commentable_type. يمثل الحقل commentable_id المفتاح الأجنبي للمنشور (post) أو الفيديو (video)، بينما يمثل الحقل commentable_type اسم الصنف المالك للعلاقة، حيث يحدد Eloquent أي من النماذج يجب أن تعاد من العلاقة بناءً على الحقل commentable_type.

هيكلية النماذج

لنطّلع الآن على تعريفات النماذج المطلوبة لتعريف هذه العلاقة:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
   /**
    * قراءة جميع السجلات القابلة للتعليق.
    */
   public function commentable()
   {
       return $this->morphTo();
   }
}


class Post extends Model
{
   /**
    * قراءة تعليقات المنشور.
    */
   public function comments()
   {
       return $this->morphMany('App\Comment', 'commentable');
   }
}


class Video extends Model
{
   /**
    * قراءة تعليقات الفيديو.
    */
   public function comments()
   {
       return $this->morphMany('App\Comment', 'commentable');
   }
}

استرداد العلاقات متعددة الأشكال

بعد تعريف الجداول والنماذج، يمكنك الوصول للعلاقات باستخدام النماذج. مثلًا، للوصول إلى تعليقات منشور ما، يمكننا استخدام الخاصية الديناميكية comments:

$post = App\Post::find(1);

foreach ($post->comments as $comment) {

  //

}

يمكنك أيضًا الوصول إلى مالك العلاقة متعددة الأشكال من النموذج متعدد الأشكال عن طريق الوصول إلى اسم التابع الذي يستدعي morphTo، وهو التابع commentable الموجود في النموذج Comment في حالتنا. إذًا يمكننا الوصول إليه كخاصية ديناميكية:

$comment = App\Comment::find(1);

$commentable = $comment->commentable;

ستعيد العلاقة commentable إما كائن Post أو كائن Video بناءً على نوع مالك العلاقة.

أنواع مخصصة للعلاقات متعددة الأشكال

افتراضيًّا، يستخدم Laravel الاسم الكامل للصنف لتخزينه اسمًا لنوع النموذج المرتبط. مثلًا، بالنظر إلى المثال أعلاه الذي يكون فيه التعليق Comment مرتبطًا إما بمنشور Post أو فيديو Video، يحتوي الحقل commentable_type إما على القيمة App\Post أو القيمة App\Video. لكن يمكنك تجاوز هذا العرف للفصل بين قاعدة البيانات وهيكلية تطبيقك الداخلية. في هذه الحالة، يمكن تعريف ما يسمى بجدول الأشكال للعلاقة، وذلك لإخبار Eloquent باستخدام أسماء مختلفة لكل نموذج بدلًا من اسم صنف النموذج نفسه:

use Illuminate\Database\Eloquent\Relations\Relation;

Relation::morphMap([
   'posts' => 'App\Post',
   'videos' => 'App\Video',
]);

يمكنك تسجيل الـ morphMap في التابع boot الخاص بمزود الخدمة الرئيسي AppServiceProvider، أو إنشاء مزود خدمة منفصل إذا أردت.

علاقات الكثير إلى كثير متعددة الأشكال (Many To Many Polymorphic Relationships)

هيكلية الجداول

إضافةً إلى العلاقات متعددة الأشكال التقليدية، يمكنك تعريف علاقات "كثير إلى كثير" متعددة الأشكال. مثلًا، قد يشترك النموذجان Post و Video بعلاقة متعددة الأشكال إلى نموذج الوسم Tag. باستخدام علاقات الكثير إلى كثير متعددة الأشكال، يمكنك الحصول على قائمة وحيدة بالوسوم (tags) الفريدة المشتركة بين المنشورات (posts) والفيديوهات (videos). لنطّلع أولاً على هيكلية الجداول:

posts
   id - integer
   name - string


videos
   id - integer
   name - string


tags
   id - integer
   name - string


taggables
   tag_id - integer
   taggable_id - integer
   taggable_type - string

هيكلية النماذج

الآن، يمكننا تعريف العلاقة على النماذج. سيمتلك النموذجان Post و Video التابع tags الذي يستدعي التابع morphToMany:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
   /**
    * قراءة وسوم المنشور.
    */
   public function tags()
   {
       return $this->morphToMany('App\Tag', 'taggable');
   }
}

تعريف معكوس العلاقة

الآن، يمكنك تعريف تابع لكل نموذج مرتبط على النموذج Tag. لذا في هذا المثال، سنعرّف التابعين posts و videos:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
   /**
    * قراءة كل المنشورات التي تملك هذا الوسم.
    */
   public function posts()
   {
       return $this->morphedByMany('App\Post', 'taggable');
   }

   /**
    * قراءة كل الفيديوهات التي تملك هذا الوسم.
    */
   public function videos()
   {
       return $this->morphedByMany('App\Video', 'taggable');
   }
}

استرداد العلاقة

بعد تعريف الجداول والنماذج، يمكنك الوصول للعلاقة باستخدام نماذجك. مثلًا، للوصول إلى الوسوم المرتبطة بمنشور ما، يمكنك استخدام الخاصية الديناميكية tags:

$post = App\Post::find(1);

foreach ($post->tags as $tag) {

  //

}

يمكنك أيضًا الوصول إلى مالك العلاقة متعددة الأشكال من النموذج متعدد الأشكال وذلك بالوصول إلى اسم التابع الذي ينفذ استدعاء للتابع morphedByMany. في هذه الحالة، يمكن الوصول لأحد التابعين posts أو videos على النموذج Tag. لذلك، سنصل إلى هذه التوابع كخاصيات ديناميكية:

$tag = App\Tag::find(1);

foreach ($tag->videos as $video) {

  //

}

الاستعلام عن العلاقات

بما أن جميع أنواع العلاقات المعرفة بـ Eloquent تكون توابع، يمكنك استدعاء هذه التوابع للوصول إلى كائن العلاقة دون تنفيذ استعلام العلاقة بحد ذاته. علاوةً على ذلك، جميع أنواع علاقات Eloquent تخدم كمنشئي استعلامات، مما يمكّنك من سلسلة قيود الاستعلام على استعلام العلاقة قبل تنفيذ الاستعلام البنيوي SQL الأخير على قاعدة البيانات.

مثلًا، لنتخيل نظام مدونة يكون فيه نموذج المستخدم User مرتبطًا بأكثر من منشور Post:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
   /**
    * قراءة جميع منشورات المستخدم.
    */
   public function posts()
   {
       return $this->hasMany('App\Post');
   }
}

يمكنك الاستعلام عن العلاقة posts وإضافة قيود إضافية على العلاقة كالتالي:

$user = App\User::find(1);

$user->posts()->where('active', 1)->get();

يمكنك استخدام أي تابع من توابع منشئ الاستعلامات على العلاقة، لذا تأكد من قراءتك لتوثيق منشئ الاستعلامات لتعلم هذه التوابع المتاحة للاستخدام.

توابع العلاقات والخاصيات الديناميكية

في حال لم ترد أن تضف قيودًا إضافية للاستعلام العلائقي، يمكنك الوصول إلى العلاقة كما وأنها خاصية. بالاستكمال مع مثال المستخدم والمنشورات، يمكنك الوصول إلى منشورات المستخدم كالتالي:

$user = App\User::find(1);

foreach ($user->posts as $post) {

  //

}

إن الخاصيات الديناميكية تتبع "التحميل الكسول (Lazy Loading)"، مما يعني أنه ستُحمّل العلاقة فقط عندما تحتاجها. بسبب ذلك، يلجأ المبرمجون لتحميل يسمى بـ "التحميل الحثيث (Eager Loading)" لتحميل العلاقات بشكل مسبق. يقلل التحميل الحريص من كمية الاستعلامات الفعلية التي  تُنفّذ على قاعدة البيانات، مما يخدم في تسريع عمل التطبيق.

الاستعلام عن وجود العلاقة

عند الوصول إلى سجل من نموذج، قد تحتاج إلى تحديد نتائجك بناءً على وجود أحد العلاقات. مثلًا، لنفرض أنك تريد استرداد كل المنشورات التي تملك تعليقًا واحدًا على الأقل. لتحقيق ذلك، يمكنك تمرير اسم العلاقة للتوابع has و orHas:

// استرداد جميع المنشورات التي تملك تعلقًا واحدًا على الأقل..
$posts = App\Post::has('comments')->get();

يمكنك أيضًا تحديد عملية ورقم لتخصيص الاستعلام بشكل أكبر:

// استرداد جميع المنشورات التي تملك ثلاث تعليقات أو أكثر..
$posts = App\Post::has('comments', '>=', 3)->get();

يمكن تحقيق تعليمات الـ has المتداخلة أيضًا باستخدام نمط النقطة (dot). مثلًا، لاسترداد جميع المنشورات التي تملك تعليقًا واحدًا وصوتًا واحدًا على الاقل:

// استرداد جميع المنشورات التي تملك تعليقاً واحداً على الأقل وصوتاً..
$posts = App\Post::has('comments.votes')->get();

إذا أردت أن تحظى بقوة أكبر، يمكنك استخدام التابعين whereHas و orWhereHas لإضافة قيود where على استعلامات has. تمكّنك هذه التوابع من إضافة قيود مخصصة على قيود العلاقة، كالتحقق من محتوى التعليق:

// استرداد جميع المنشورات التي تملك تعليقًا واحدًا على الأقل يحتوي على كلمات مثل foo%..
$posts = App\Post::whereHas('comments', function ($query) {
   $query->where('content', 'like', 'foo%');
})->get();

الاستعلام عن عدم وجود العلاقة

عند الوصول إلى سجل من نموذج، قد تحتاج إلى تحديد نتائجك بناءً على عدم وجود أحد العلاقات. مثلًا، لنفرض أنك تريد استرداد كل المنشورات التي لا تملك أي تعليق. لتحقيق ذلك، يمكنك تمرير اسم العلاقة للتوابع doesntHave و orDoesntHave:

$posts = App\Post::doesntHave('comments')->get();

إذا أردت أن تحظى بقوة أكبر، يمكنك استخدام التابعين whereDoesntHave و orWhereDoesntHave لإضافة قيود where على استعلامات doesntHave. تمكّنك هذه التوابع من إضافة قيود مخصصة على قيود العلاقة، كالتحقق من محتوى التعليق:

$posts = App\Post::whereDoesntHave('comments', function ($query) {
   $query->where('content', 'like', 'foo%');
})->get();

يمكن تحقيق التعليمات المتداخلة أيضًا باستخدام نمط النقطة (dot). مثلًا، يستردّ الاستعلام التالي كل المنشورات مع التعليقات المنشأة من معلّقين غير محظورين:

$posts = App\Post::whereDoesntHave('comments.author', function ($query) {
   $query->where('banned', 1);
})->get();

تعداد النماذج المرتبطة

إذا أردت الحصول على عدد النماذج المرتبطة دون تحميلها، يمكنك استخدام التابع withCount، الذي سيضيف حقل count_{علاقة} للنموذج الناتج. مثال:

$posts = App\Post::withCount('comments')->get();

foreach ($posts as $post) {
   echo $post->comments_count;
}

يمكنك إضافة "العدادات" إلى أكثر من علاقة واحدة، ويمكنك إضافة شروط للعدادات:

$posts = App\Post::withCount(['votes', 'comments' => function ($query) {
   $query->where('content', 'like', 'foo%');
}])->get();

echo $posts[0]->votes_count;

echo $posts[0]->comments_count;

يمكنك أيضًا تخصيص اسم حقل العداد الناتج، مما يتيح لك إضافة أكثر من عداد لنفس العلاقة:

$posts = App\Post::withCount([
   'comments',
   'comments as pending_comments_count' => function ($query) {
       $query->where('approved', false);
   }
])->get();

echo $posts[0]->comments_count;

echo $posts[0]->pending_comments_count;

التحميل الحثيث Eager loading

عند الوصول لعلاقات Eloquent كخاصيات، تُحمّل بيانات العلاقة بشكل كسول، مما يعني عدم تحميل البيانات ريثما نصل إلى خاصية العلاقة. لكن، يمكن لـ Eloquent أن يحمّل العلاقات بشكل "حثيث" أو مسبق عند الاستعلام عن العلاقة الأب. يؤدي التحميل الحريص إلى إلغاء مشكلة الـ N + 1 استعلام. لتمثيل هذه المشكلة، لنفرض نموذج كتاب Book مرتبط بكاتب Author:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
   /**
    * Get the author that wrote the book.
    */
   public function author()
   {
       return $this->belongsTo('App\Author');
   }
}

لنستردّ الآن جميع الكتب وكتّابها:

$books = App\Book::all();

foreach ($books as $book) {
   echo $book->author->name;
}

سينفّذ هذا التكرار استعلام واحد لاسترداد كل الكتب، واستعلام واحد من أجل كل كتاب لاسترداد الكاتب. لذا إذا كان لدينا 25 كتاب، سيقوم هذا التكرار بتنفيذ 26 استعلام: 1 من أجل استرداد كل الكتب، و 25 استعلام من أجل استرداد كاتب كل كتاب. لحسن الحظ، يمكن استخدام التحميل الحثيث لتقليل عدد الاستعلامات هذا إلى استعلامين فقط. عند الاستعلام، يمكنك تحديد العلاقة التي ستُحمّل بشكل حثيث باستخدام التابع with:

$books = App\Book::with('author')->get();

foreach ($books as $book) {
   echo $book->author->name;
}

من أجل هذه العملية، سينفّذ استعلامين فقط:

select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

التحميل الحثيث لأكثر من علاقة

قد تحتاج بعض الأوقات إلى أن تتحمل التحميل الحثيث لأكثر من علاقة باستخدام عملية واحدة. لتحقيق ذلك، مرّر عدة وسائط للتابع with:

$books = App\Book::with(['author', 'publisher'])->get();

التحميل الحثيث المتداخل

للتحميل الحثيث للعلاقات المتداخلة، يمكنك استخدام صيغة النقطة (dot). على سبيل المثال، لنحمل كل كتّاب الكتب وكل معلومات الاتصال الشخصية الخاصة بالكتّاب، وذلك في عملية Eloquent وحيدة:

$books = App\Book::with('author.contacts')->get();

التحميل الحثيث لحقول محددة

قد تحتاج بعض الأوقات إلى تحميل حقول محددة فقط ضمن عملية التحميل الحثيث لنماذجك. لهذا السبب، يتيح Eloquent تحديد الحقول التي تريد أن تحمل من العلاقة:

$users = App\Book::with('author:id,name')->get();

ملاحظة: عند استخدام هذه الميزة، يجب دائمًا تحميل الحقل id من العلاقة التي تحمّلها.

إضافة القيود إلى التحميل الحثيث

قد تحتاج بعض الأوقات إلى التحميل الحثيث لبعض العلاقات، وإلى إضافة قيود إضافية لاستعلام التحميل الحثيث. إليك مثال:

$users = App\User::with(['posts' => function ($query) {
   $query->where('title', 'like', '%first%');
}])->get();

في هذا المثال، سيقوم Eloquent بالتحميل الحريص للمنشورات التي تملك عنوانًا (title) محتويًا للكلمة first. بالطبع، يمكنك استخدام توابع منشئ الاستعلامات الأخرى لتخصيص عملية التحميل الحثيث بشكل أكبر:

$users = App\User::with(['posts' => function ($query) {
   $query->orderBy('created_at', 'desc');
}])->get();

التحميل الحثيث الكسول

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

$books = App\Book::all();

if ($someCondition) {
   $books->load('author', 'publisher');
}

إذا أردت إضافة قيود إضافية على استعلام التحميل الحثيث، يمكنك تمرير مصفوفة تمثّل مفاتيحها أسماء العلاقات التي تريد تحميلها، وقيمها تحمل نطاقات مغلقة تستقبل كائنًا من الاستعلام:

$books->load(['author' => function ($query) {
   $query->orderBy('published_date', 'asc');
}]);

لتحميل علاقة ما فقط إن لم تُحمّل مسبقًا، يمكنك استخدام التابع loadMissing:

public function format(Book $book)
{
   $book->loadMissing('author');

   return [
       'name' => $book->name,
       'author' => $book->author->name
   ];
}

إضافة وتعديل النماذج المرتبطة

تابع الحفظ Save

يزوّد Eloquent بتوابع مساعدة لإضافة سجلات جديدة للعلاقات. مثلًا، قد تحتاج إلى إضافة تعليق (Comment) جديد إلى منشور ما (Post). بدلًا من تعيين الحقل post_id يدويًّا على سجل Comment، يمكنك تمرير التعليق مباشرةً إلى التابع save الخاص بالعلاقة:

$comment = new App\Comment(['message' => 'A new comment.']);

$post = App\Post::find(1);

$post->comments()->save($comment);

لاحظ أننا لم نصل إلى العلاقة comments كخاصية ديناميكية. بدلًا من ذلك، استدعينا التابع comments للحصول على كائن العلاقة. يضيف التابع save الحقل post_id المناسب إلى السجل Comment الجديد تلقائيًّا . إذا أردت حفظ مجموعة من السجلات المرتبطة، يمكنك استخدام التابع saveMany:

$post = App\Post::find(1);

$post->comments()->saveMany([
   new App\Comment(['message' => 'A new comment.']),
   new App\Comment(['message' => 'Another comment.']),
]);

تابع الإنشاء Create

إضافةً إلى التوابع save و saveMany، يمكنك استخدام التابع create الذي يقبل مصفوفة من الخواص وينشئ السجل ويضيفه إلى قاعدة البيانات. مجددًا، الفرق بين التابعين save و create هو أن التابع save يقبل كائن من نموذج Eloquent كامل، بينما التابع create يقبل مصفوفة PHP بسيطة:

$post = App\Post::find(1);

$comment = $post->comments()->create([
   'message' => 'A new comment.',
]);

ملاحظة: قبل استخدام التابع create، لا تنسَ مراجعة التوثيق الخاص بالتعيين الجماعي للخاصيات. يمكنك استخدام التابع createMany لإنشاء مجموعة من السجلات المرتبطة:

$post = App\Post::find(1);

$post->comments()->createMany([
   [
       'message' => 'A new comment.',
   ],
   [
       'message' => 'Another new comment.',
   ],
]);

علاقة الانتماء (Belongs To)

عند تحديث علاقة انتماء (belongsTo)، يمكنك استخدام التابع associate. يعيّن هذا التابع المفتاح الأجنبي على السجل الابن:

$account = App\Account::find(10);

$user->account()->associate($account);

$user->save();

عند إزالة علاقة انتماء، يمكنك استخدام التابع dissociate. يعيّن هذا التابع المفتاح الأجنبي إلى null:

$user->account()->dissociate();

$user->save();

النماذج الافتراضية

يمكن لعلاقة الانتماء أن تحدد نموذج افتراضي للعلاقة المعادة عندما تكون null. يدعى ذلك النمط بنمط الكائنات الفارغة null، ويساعد على إزالة القيود الإضافية في برنامجك. في المثال التالي، تعيد العلاقة user كائنًا من App\User فارغًا عند عدم وجود مستخدم مرتبط بالمنشور:

/**
* قراءة كاتب المنشور.
*/
public function user()
{
   return $this->belongsTo('App\User')->withDefault();
}

لإضافة حقول افتراضية على النموذج الافتراضي، يمكنك تمرير مصفوفة أو نطاق مغلق إلى التابع withDefault:

/**
* قراءة كاتب المنشور.
*/
public function user()
{
   return $this->belongsTo('App\User')->withDefault([
       'name' => 'Guest Author',
   ]);
}

/**
* قراءة كاتب المنشور.
*/
public function user()
{
   return $this->belongsTo('App\User')->withDefault(function ($user) {
       $user->name = 'Guest Author';
   });
}

العلاقات كثير إلى كثير Many to Many

الربط / فك الربط

يزود Eloquent أيضًا بمجموعة من التوابع المساعدة لجعل العمل مع السجلات المرتبطة سهلًا. مثلًا، لنفرض أنّه يمكن للمستخدم أن يملك عدة أدوار، ولكل دور عدة مستخدمين. لربط المستخدم بدور عن طريق إضافة سجل إلى الجدول الوسيطي الذي يربط العلاقة، يمكن استخدام التابع attach:

$user = App\User::find(1);

$user->roles()->attach($roleId);

عند ربط النموذج بعلاقة، يمكنك تمرير مصفوفة بالخاصيات التي ستُضاف إلى الجدول الوسيطي:

$user->roles()->attach($roleId, ['expires' => $expires]);

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

// إزالة دور واحد من المستخدم...
$user->roles()->detach($roleId);

// إزالة جميع أدوار المستخدم...
$user->roles()->detach();

للسهولة، يقبل التابعان attach و detach مصفوفة من المفاتيح الرئيسية IDs:

$user = App\User::find(1);

$user->roles()->detach([1, 2, 3]);

$user->roles()->attach([
   1 => ['expires' => $expires],
   2 => ['expires' => $expires]
]);

مزامنة الارتباطات

يمكنك استخدام التابع sync لإنشاء علاقات الكثير إلى كثير. يقبل التابع sync مصفوفة من المفاتيح الرئيسية لوضعها في الجدول الوسيطي. ستُزال المفاتيح الرئيسية غير الموجودة في المصفوفة الممررة من الجدول الوسيطي. لذا بعد استكمال العملية، سيُحتفظ بالمفاتيح الرئيسية الممررة للتابع بالمصفوفة فقط ضمن الجدول الوسيطي:

$user->roles()->sync([1, 2, 3]);

يمكنك أيضًا تمرير خاصيات إضافية للجدول الوسيطي مع المفاتيح:

$user->roles()->sync([1 => ['expires' => true], 2, 3]);

إذا لم ترد من التابع sync أن يفك الارتباط، وإنما فقط إضافة المفاتيح الجديدة، يمكنك استخدام التابع syncWithoutDetaching:

$user->roles()->syncWithoutDetaching([1, 2, 3]);

تبديل الارتباطات

تزود علاقات الكثير إلى كثير بتابع toggle الذي يبدل حالة الارتباط للمفاتيح المعطاة. في حال كان المفتاح المعطى مرتبطًا، سيفك ارتباطه؛ بالمثل، في حال كان غير مرتبط، سيربط:

$user->roles()->toggle([1, 2, 3]);

حفظ بيانات إضافية في الجدول الوسيطي

عند العمل مع علاقات الكثير إلى كثير، يقبل التابع save مصفوفة بالخاصيات الإضافية التي يجب إضافتها للجدول الوسيطي، وذلك بالوسيط الثاني للتابع:

App\User::find(1)->roles()->save($role, ['expires' => $expires]);

تعديل سجل في الجدول الوسيطي

إذا أردت تعديل سجل موجود مسبقًا في الجدول الوسيطي، يمكنك استخدام التابع updateExistingPivot. يقبل هذا التابع المفتاح الأجنبي لسجل الجدول الوسيطي، ومصفوفة الخاصيات الوسيطية:

$user = App\User::find(1);

$user->roles()->updateExistingPivot($roleId, $attributes);

تعديل بصمات الوقت للسجلات الآباء

عند تكوين علاقة انتماء belongsTo أو انتماء متعدد belongsToMany، كعلاقة التعليق والمنشور، من الضروري أحيانًا أن تُعدّل بصمات الوقت للأب عند تعديل السجل الابن. مثلًا، عند تعديل سجل Comment، يمكنك تلقائيًّا تعديل الحقل الزمني updated_at الخاص بالمنشور Post الأب للتعليق. لتحقيق ذلك، أضف الخاصية touches إلى النموذج الابن التي تحتوي مصفوفة بأسماء العلاقات التي يجب تعديل حقولها الزمنية تلقائيًّا:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
   /**
    * العلاقات التي يجب أن تعدل حقولها الزمنية.
    *
    * @var array
    */
   protected $touches = ['post'];

   /**
    * قراءة المنشور المالك للتعليق.
    */
   public function post()
   {
       return $this->belongsTo('App\Post');
   }
}

الآن، عند تعديل سجل تعليق Comment، سيعدل الحقل updated_at الخاص بالمنشور Post المالك للتعليق:

$comment = App\Comment::find(1);

$comment->text = 'Edit to this comment!';

$comment->save();

مصادر