Observers في Laravel: تنظيم منطق الأحداث بعيدًا عن الكونترولر

مقدمة

لنفترض أن لدينا في المشروع فورم تسجيل مستخدمين، ونريد أنه عند أي عملية تسجيل، أو تعديل، أو حذف، أن يتم تنفيذ إجراء معين تلقائيًا — مثل إرسال إشعار لمدير الموقع عند تسجيل مستخدم جديد:

public function store(Request $request)
{
    $user = User::create([
        'name' => $request->name,
        'email' => $request->email,
        'password' => Hash::make($request->password)
    ]);

    $user->notify(new NewUser());

    return $user;
}

الكود أعلاه صحيح ويعمل، لكنه يخالف مبدأ المسؤولية الواحدة (Single Responsibility Principle)، حيث يُفترض أن تكون دالة store مسؤولة عن شيء واحد فقط، وهو حفظ بيانات المستخدم — لا عن إرسال الإشعارات أيضًا.

من هنا تبرز أهمية Observers في Laravel، كحل أنيق لفصل منطق الاستجابة لأحداث الموديل عن الكونترولر.


ما هو Observer

حسب التوثيق الرسمي لـ Laravel، عند الاستماع لعدة أحداث (Events) على موديل معين، يمكن استخدام Observer لتجميع كل هذه المستمعات (Listeners) داخل كلاس واحد. تحمل دوال الـ Observer أسماء تعكس أحداث Eloquent المطلوب الاستماع إليها، وتستقبل كل دالة من هذه الدوال الموديل نفسه كوسيط وحيد.

باختصار، يساعدنا Observer في:

  • الحفاظ على الكونترولر نظيفًا (Clean Controller) ومركّزًا على وظيفته الأساسية فقط.
  • تنفيذ أي إجراء تلقائيًا عند حدوث تغيير على الموديل (إنشاء، تعديل، حذف...) بغض النظر عن مصدر هذا التغيير في الكود.
  • تجميع كل منطق الاستجابة لأحداث موديل معين في مكان واحد، بدلاً من توزيعه بين عدة كونترولرات.

كيفية إنشاء Observer

يمكن إنشاء Observer جديد عبر أمر Artisan التالي:

php artisan make:observer UserObserver --model=User
  • UserObserver: اسم كلاس الـ Observer.
  • --model=User: تحديد الموديل الذي سيتم ربط الـ Observer به.

بعد تنفيذ الأمر، يتم إنشاء مجلد جديد باسم Observers داخل مجلد app، وبداخله كلاس UserObserver الذي يحتوي على الدوال التالية افتراضيًا:

<?php

namespace App\Observers;

use App\Models\User;

class UserObserver
{
    public function created(User $user)
    {
        //
    }

    public function updated(User $user)
    {
        //
    }

    public function deleted(User $user)
    {
        //
    }

    public function restored(User $user)
    {
        //
    }

    public function forceDeleted(User $user)
    {
        //
    }
}

بالإضافة إلى هذه الدوال، يمكن أيضًا إضافة دوال أخرى غير موجودة افتراضيًا في الكلاس، مثل: retrieved، creating، updating، saving، saved، deleting، restoring.

متى يتم تنفيذ كل دالة

الدالةمتى يتم تنفيذها
retrievedعند جلب سجل من قاعدة البيانات، مثلاً عند استدعاء Model::findOrFail($id)
creatingعند حفظ سجل جديد، لكن قبل إتمام الحفظ فعليًا (قبل تعيين id وtimestamps)
createdبعد حفظ السجل الجديد بنجاح في قاعدة البيانات
updatingأثناء عملية التعديل، لكن قبل تنفيذ التعديل فعليًا في قاعدة البيانات
updatedبعد إتمام عملية التعديل بنجاح
saving / savedتُنفَّذ عند الإنشاء والتعديل معًا. عند الإنشاء: saving ثم creating ثم created ثم saved. عند التعديل: saving ثم updating ثم updated ثم saved
deletingأثناء عملية الحذف، قبل إتمامها
deletedبعد إتمام عملية الحذف بنجاح
restoring / restoredعند استرجاع سجل محذوف (باستخدام Soft Delete)

تسجيل الـ Observer

إنشاء كلاس الـ Observer وحده لا يكفي لتفعيله؛ فلا بد من تسجيله حتى يبدأ Laravel بالاستماع لأحداث الموديل المرتبط به. يتم ذلك داخل ملف AppServiceProvider.php، ضمن الدالة boot:

public function boot()
{
    User::observe(new UserObserver());
}

مثال عملي أول: إرسال إشعار عند تسجيل مستخدم جديد

قبل استخدام Observer

public function store(Request $request)
{
    $user = User::create([
        'name' => $request->name,
        'email' => $request->email,
        'password' => Hash::make($request->password)
    ]);

    $user->notify(new NewUser());

    return $user;
}

بعد استخدام Observer

الكونترولر يصبح مسؤولًا فقط عن إنشاء المستخدم:

public function store(Request $request)
{
    return User::create([
        'name' => $request->name,
        'email' => $request->email,
        'password' => Hash::make($request->password)
    ]);
}

بينما يتم نقل منطق إرسال الإشعار إلى داخل UserObserver، ضمن الدالة created (لأننا نريد تنفيذ الإشعار بعد نجاح عملية الحفظ):

public function created(User $user)
{
    $user->notify(new NewUser());
}

بهذا الشكل، أصبحت دالة store مسؤولة عن إنشاء المستخدم فقط، بينما أصبح UserObserver مسؤولًا عن أي إجراء يجب تنفيذه بعد الإنشاء، بغض النظر عن مصدر عملية الإنشاء (سواء من هذا الكونترولر، أو من Seeder، أو من أي مكان آخر في المشروع).


مثال عملي ثانٍ: حذف تلقائي للتعليقات عند حذف مقال

لنفترض أن لدينا Article Model وComment Model، بحيث يملك كل مقال مجموعة من التعليقات، ونريد أنه عند حذف مقال معين، يتم حذف كل التعليقات التابعة له تلقائيًا.

أولًا، نحتاج لتعريف العلاقة بين الموديلين:

public function comments()
{
    return $this->hasMany(Comment::class);
}

الحذف قبل استخدام Observer

في هذه الحالة، تكون دالة destroy مسؤولة عن حذف المقال وحذف التعليقات التابعة له معًا:

public function destroy(Article $article)
{
    $article->delete();
    $article->comments()->delete();
}

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

الحذف بعد استخدام Observer

إنشاء ArticleObserver

php artisan make:observer ArticleObserver --model=Article

تسجيل ArticleObserver في AppServiceProvider ضمن الدالة boot:

public function boot()
{
    Article::observe(new ArticleObserver());
}

بما أننا نتعامل مع عملية حذف، نذهب إلى الدالة deleted داخل ArticleObserver (أو deleting إذا أردنا تنفيذ الحذف قبل إتمام حذف المقال نفسه)، ونضع فيها منطق حذف التعليقات:

public function deleted(Article $article)
{
    $article->comments()->delete();
}

تنظيف ArticleController

نعود إلى دالة destroy ونزيل منها سطر حذف التعليقات، لتصبح مسؤولة عن حذف المقال فقط:

public function destroy(Article $article)
{
    $article->delete();
}

ملاحظة: deleted أم deleting؟

في المثال السابق تم استخدام deleted (بعد إتمام الحذف)، وهذا مناسب هنا لأن حذف التعليقات لا يعتمد على وجود المقال نفسه بعد الحذف. لكن في حالات أخرى، قد يكون من الضروري استخدام deleting (قبل إتمام الحذف) بدلاً من deleted، خصوصًا إذا كان الإجراء المطلوب يحتاج إلى الوصول لبيانات مرتبطة بالمقال لا تزال موجودة فقط قبل حذفه فعليًا من قاعدة البيانات.


جدول ملخّص لأهم دوال الـ Observer

الدالةالاستخدام الشائع
createdتنفيذ إجراء بعد إنشاء سجل جديد بنجاح (إرسال إشعار، تسجيل نشاط...)
updatedتنفيذ إجراء بعد تعديل سجل بنجاح (تحديث ذاكرة تخزين مؤقت، إرسال إشعار تغيير...)
deleting / deletedتنفيذ إجراءات مرتبطة بحذف سجل، مثل حذف السجلات التابعة له
restoredتنفيذ إجراء عند استرجاع سجل محذوف عبر Soft Delete
observe()تسجيل الـ Observer وربطه بالموديل داخل AppServiceProvider

الخلاصة

توفّر Observers في Laravel طريقة نظيفة ومنظمة للاستجابة لأحداث الموديل (إنشاء، تعديل، حذف، استرجاع...) دون حشو هذا المنطق داخل الكونترولرات. باستخدام Observer:

  • تبقى الكونترولرات مركّزة على مسؤوليتها الأساسية فقط.
  • يصبح منطق الاستجابة للأحداث مركزيًا وقابلًا لإعادة الاستخدام من أي مكان في المشروع (كونترولر، Seeder، Job، Command...).
  • يسهُل اختبار وصيانة هذا المنطق بمعزل عن باقي الكود.

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