Laravel المتقدم

Eloquent المتقدم: الأحداث والمراقبين

18 دقيقة الدرس 2 من 40

فهم أحداث Eloquent

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

أحداث النموذج المتاحة:
  • retrieved - بعد استرجاع النموذج من قاعدة البيانات
  • creating - قبل حفظ نموذج جديد في قاعدة البيانات
  • created - بعد حفظ نموذج جديد في قاعدة البيانات
  • updating - قبل تحديث نموذج موجود
  • updated - بعد تحديث نموذج موجود
  • saving - قبل إنشاء أو تحديث نموذج
  • saved - بعد إنشاء أو تحديث نموذج
  • deleting - قبل حذف نموذج
  • deleted - بعد حذف نموذج
  • restoring - قبل استعادة نموذج محذوف بشكل ناعم
  • restored - بعد استعادة نموذج محذوف بشكل ناعم
  • replicating - قبل تكرار نموذج

الاستماع للأحداث في النماذج

أبسط طريقة للاستماع لأحداث النموذج هي استخدام طريقة boot() أو booted() في نموذجك.

مستمعي الأحداث الأساسية:
// app/Models/Post.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class Post extends Model
{
    protected static function booted()
    {
        // توليد slug عند إنشاء منشور جديد
        static::creating(function ($post) {
            if (empty($post->slug)) {
                $post->slug = Str::slug($post->title);
            }
        });

        // تحديث slug عند تغيير العنوان
        static::updating(function ($post) {
            if ($post->isDirty('title')) {
                $post->slug = Str::slug($post->title);
            }
        });

        // زيادة عدد المشاهدات عند استرجاع المنشور
        static::retrieved(function ($post) {
            // كن حذراً مع هذا - يمكن أن يسبب مشاكل في الأداء
            // من الأفضل استخدام قائمة انتظار أو تحديث دفعي
        });

        // التسجيل عند حذف المنشور
        static::deleted(function ($post) {
            \Log::info("Post deleted: {$post->title}");
        });
    }
}
استخدام Closures للمنطق المعقد:
// app/Models/Order.php
namespace App\Models;

use App\Notifications\OrderCreatedNotification;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    protected static function booted()
    {
        // إرسال إشعار عند إنشاء الطلب
        static::created(function ($order) {
            $order->user->notify(new OrderCreatedNotification($order));
        });

        // حساب الإجماليات قبل الحفظ
        static::saving(function ($order) {
            $order->total = $order->items->sum(function ($item) {
                return $item->quantity * $item->price;
            });
            $order->tax = $order->total * 0.1; // ضريبة 10٪
            $order->grand_total = $order->total + $order->tax;
        });

        // تحديث المخزون عند اكتمال الطلب
        static::updated(function ($order) {
            if ($order->wasChanged('status') && $order->status === 'completed') {
                foreach ($order->items as $item) {
                    $item->product->decrement('stock', $item->quantity);
                }
            }
        });

        // منع حذف الطلبات المكتملة
        static::deleting(function ($order) {
            if ($order->status === 'completed') {
                return false; // إلغاء عملية الحذف
            }
        });
    }
}
قيم إرجاع مستمع الحدث: إذا أرجع مستمع الحدث false، فسيتم إلغاء العملية. هذا مفيد لأحداث creating و updating و saving و deleting لمنع اكتمال الإجراء.

مراقبو النماذج (Model Observers)

لمعالجة الأحداث الأكثر تعقيداً، توفر Laravel المراقبين. المراقبون هم فئات تحتوي على طرق تتوافق مع أحداث النموذج، مما يحافظ على نموذجك نظيفاً ويفصل الاهتمامات.

إنشاء مراقب:
// توليد مراقب باستخدام artisan
php artisan make:observer PostObserver --model=Post

// app/Observers/PostObserver.php
namespace App\Observers;

use App\Models\Post;
use Illuminate\Support\Str;

class PostObserver
{
    /**
     * معالجة حدث Post "creating".
     */
    public function creating(Post $post): void
    {
        // توليد slug تلقائياً
        if (empty($post->slug)) {
            $post->slug = Str::slug($post->title);
        }

        // تعيين المؤلف الافتراضي إذا لم يتم توفيره
        if (empty($post->author_id)) {
            $post->author_id = auth()->id();
        }
    }

    /**
     * معالجة حدث Post "created".
     */
    public function created(Post $post): void
    {
        // تسجيل الإنشاء
        \Log::info("New post created: {$post->title}");

        // إرسال إشعار للمتابعين
        $post->author->followers->each(function ($follower) use ($post) {
            $follower->notify(new \App\Notifications\NewPostPublished($post));
        });
    }

    /**
     * معالجة حدث Post "updating".
     */
    public function updating(Post $post): void
    {
        // تحديث slug إذا تغير العنوان
        if ($post->isDirty('title')) {
            $post->slug = Str::slug($post->title);
        }

        // تسجيل من قام بالتحديث
        $post->last_updated_by = auth()->id();
    }

    /**
     * معالجة حدث Post "updated".
     */
    public function updated(Post $post): void
    {
        // مسح الذاكرة المؤقتة عند تحديث المنشور
        \Cache::forget("post.{$post->id}");
        \Cache::forget("posts.recent");

        // إرسال إشعار إذا تغيرت حالة النشر
        if ($post->wasChanged('status') && $post->status === 'published') {
            $post->author->notify(new \App\Notifications\PostPublished($post));
        }
    }

    /**
     * معالجة حدث Post "deleting".
     */
    public function deleting(Post $post): void
    {
        // حذف التعليقات المرتبطة
        $post->comments()->delete();

        // حذف الوسائط المرتبطة
        $post->media()->each(function ($media) {
            \Storage::delete($media->path);
            $media->delete();
        });
    }

    /**
     * معالجة حدث Post "deleted".
     */
    public function deleted(Post $post): void
    {
        // مسح الذاكرة المؤقتة
        \Cache::forget("post.{$post->id}");

        // تسجيل الحذف
        \Log::warning("Post deleted: {$post->title} by user " . auth()->id());
    }

    /**
     * معالجة حدث Post "restored".
     */
    public function restored(Post $post): void
    {
        // استعادة التعليقات المرتبطة
        $post->comments()->restore();

        \Log::info("Post restored: {$post->title}");
    }
}
تسجيل مراقب:
// app/Providers/EventServiceProvider.php
namespace App\Providers;

use App\Models\Post;
use App\Observers\PostObserver;
use Illuminate\Support\ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * تسجيل أي أحداث لتطبيقك.
     */
    public function boot(): void
    {
        // تسجيل المراقب
        Post::observe(PostObserver::class);
    }
}

// بديل: التسجيل في AppServiceProvider
// app/Providers/AppServiceProvider.php
public function boot(): void
{
    Post::observe(PostObserver::class);
}
تسمية طرق المراقب: يجب أن تتطابق طرق المراقب تماماً مع أسماء الأحداث: retrieved، creating، created، updating، updated، saving، saved، deleting، deleted، restoring، restored، replicating.

أنماط المراقب المتقدمة

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

مراقب المستخدم مع ميزات متعددة:
// app/Observers/UserObserver.php
namespace App\Observers;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class UserObserver
{
    /**
     * معالجة حدث User "creating".
     */
    public function creating(User $user): void
    {
        // تجزئة كلمة المرور إذا لم تكن مجزأة بالفعل
        if ($user->password && !Hash::needsRehash($user->password)) {
            $user->password = Hash::make($user->password);
        }

        // توليد اسم مستخدم فريد إذا لم يتم توفيره
        if (empty($user->username)) {
            $base = Str::slug($user->name);
            $username = $base;
            $counter = 1;

            while (User::where('username', $username)->exists()) {
                $username = $base . $counter++;
            }

            $user->username = $username;
        }

        // توليد رمز API
        $user->api_token = Str::random(80);
    }

    /**
     * معالجة حدث User "created".
     */
    public function created(User $user): void
    {
        // إنشاء ملف تعريف افتراضي
        $user->profile()->create([
            'bio' => '',
            'avatar' => 'default-avatar.png',
        ]);

        // إنشاء إعدادات افتراضية
        $user->settings()->create([
            'notifications_enabled' => true,
            'email_frequency' => 'daily',
        ]);

        // إرسال بريد ترحيبي
        $user->sendEmailVerificationNotification();

        // تسجيل التسجيل
        activity()
            ->performedOn($user)
            ->log('User registered');
    }

    /**
     * معالجة حدث User "updating".
     */
    public function updating(User $user): void
    {
        // إعادة تجزئة كلمة المرور إذا تغيرت
        if ($user->isDirty('password')) {
            $user->password = Hash::make($user->password);
        }

        // التحقق من أن البريد الإلكتروني فريد إذا تغير
        if ($user->isDirty('email')) {
            $user->email_verified_at = null;
        }
    }

    /**
     * معالجة حدث User "deleting".
     */
    public function deleting(User $user): void
    {
        // حذف البيانات المرتبطة
        $user->posts()->delete();
        $user->comments()->delete();
        $user->profile()->delete();
        $user->settings()->delete();

        // إلغاء رموز API
        $user->tokens()->delete();

        // تسجيل الحذف
        activity()
            ->performedOn($user)
            ->log('User account deleted');
    }
}

إرسال الأحداث المخصصة

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

إنشاء أحداث مخصصة:
// app/Events/OrderShipped.php
namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderShipped
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public Order $order
    ) {}
}

// app/Events/PaymentProcessed.php
namespace App\Events;

use App\Models\Payment;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class PaymentProcessed
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public Payment $payment,
        public bool $successful
    ) {}
}
إنشاء مستمعي الأحداث:
// app/Listeners/SendShipmentNotification.php
namespace App\Listeners;

use App\Events\OrderShipped;
use App\Notifications\OrderShippedNotification;

class SendShipmentNotification
{
    public function handle(OrderShipped $event): void
    {
        $event->order->user->notify(
            new OrderShippedNotification($event->order)
        );
    }
}

// app/Listeners/UpdateInventory.php
namespace App\Listeners;

use App\Events\OrderShipped;

class UpdateInventory
{
    public function handle(OrderShipped $event): void
    {
        foreach ($event->order->items as $item) {
            $item->product->decrement('stock', $item->quantity);
        }
    }
}
تسجيل الأحداث المخصصة:
// app/Providers/EventServiceProvider.php
namespace App\Providers;

use App\Events\OrderShipped;
use App\Events\PaymentProcessed;
use App\Listeners\SendShipmentNotification;
use App\Listeners\UpdateInventory;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * تعيينات مستمع الأحداث للتطبيق.
     */
    protected $listen = [
        OrderShipped::class => [
            SendShipmentNotification::class,
            UpdateInventory::class,
        ],
        PaymentProcessed::class => [
            // إضافة المستمعين هنا
        ],
    ];

    /**
     * تسجيل أي أحداث لتطبيقك.
     */
    public function boot(): void
    {
        //
    }
}
إرسال الأحداث المخصصة:
// في controller أو الخدمة الخاصة بك
use App\Events\OrderShipped;
use App\Models\Order;

public function shipOrder(Order $order)
{
    // تحديث حالة الطلب
    $order->update([
        'status' => 'shipped',
        'shipped_at' => now(),
    ]);

    // إرسال الحدث
    event(new OrderShipped($order));

    // صيغة بديلة
    OrderShipped::dispatch($order);

    return response()->json(['message' => 'Order shipped successfully']);
}

مشتركو الأحداث (Event Subscribers)

مشتركو الأحداث يسمحون لك بالاشتراك في أحداث متعددة داخل فئة واحدة، وهي مثالية لمعالجة الأحداث ذات الصلة:

إنشاء مشترك أحداث:
// app/Listeners/UserEventSubscriber.php
namespace App\Listeners;

use App\Events\UserRegistered;
use App\Events\UserLoggedIn;
use App\Events\UserProfileUpdated;
use Illuminate\Events\Dispatcher;

class UserEventSubscriber
{
    /**
     * معالجة أحداث تسجيل المستخدم.
     */
    public function handleUserRegistration($event)
    {
        // إرسال بريد ترحيبي
        $event->user->sendWelcomeEmail();

        // إنشاء سجل نشاط
        activity()
            ->performedOn($event->user)
            ->log('User registered');
    }

    /**
     * معالجة أحداث تسجيل دخول المستخدم.
     */
    public function handleUserLogin($event)
    {
        // تحديث طابع زمني لآخر تسجيل دخول
        $event->user->update([
            'last_login_at' => now(),
            'last_login_ip' => request()->ip(),
        ]);
    }

    /**
     * معالجة أحداث تحديث ملف تعريف المستخدم.
     */
    public function handleProfileUpdate($event)
    {
        // مسح الذاكرة المؤقتة
        \Cache::forget("user.{$event->user->id}");

        // تسجيل التحديث
        activity()
            ->performedOn($event->user)
            ->log('Profile updated');
    }

    /**
     * تسجيل المستمعين للمشترك.
     */
    public function subscribe(Dispatcher $events): void
    {
        $events->listen(
            UserRegistered::class,
            [UserEventSubscriber::class, 'handleUserRegistration']
        );

        $events->listen(
            UserLoggedIn::class,
            [UserEventSubscriber::class, 'handleUserLogin']
        );

        $events->listen(
            UserProfileUpdated::class,
            [UserEventSubscriber::class, 'handleProfileUpdate']
        );
    }
}
تسجيل مشترك الأحداث:
// app/Providers/EventServiceProvider.php
namespace App\Providers;

use App\Listeners\UserEventSubscriber;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * فئات المشتركين للتسجيل.
     */
    protected $subscribe = [
        UserEventSubscriber::class,
    ];
}
اعتبار الأداء: كن حذراً مع الأحداث التي تطلق بشكل متكرر (مثل retrieved أو saving). العمليات الثقيلة في هذه المستمعات يمكن أن تؤثر بشكل كبير على الأداء. فكر في استخدام مستمعات قائمة الانتظار للمهام التي تستغرق وقتاً طويلاً.
مستمعات الأحداث في قائمة الانتظار: نفذ واجهة ShouldQueue على المستمع الخاص بك لمعالجته بشكل غير متزامن:
class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    public function handle(OrderShipped $event): void
    {
        // سيتم تشغيل هذا في قائمة انتظار
    }
}

تمرين 1: إنشاء مراقب منتج

أنشئ ProductObserver يقوم بـ:

  • توليد SKU فريد عند إنشاء منتج (إذا لم يتم توفيره)
  • توليد slug تلقائياً من اسم المنتج
  • التسجيل عند إنشاء أو تحديث أو حذف منتج
  • منع الحذف إذا كان للمنتج أي طلبات
  • مسح إدخالات الذاكرة المؤقتة ذات الصلة عند تحديث المنتج

تمرين 2: نظام أحداث مخصص

أنشئ نظام أحداث مخصص لمدونة:

  • حدث: PostPublished - يتم إرساله عند تغيير حالة المنشور إلى "published"
  • مستمع: NotifySubscribers - يرسل بريداً إلكترونياً لجميع مشتركي المدونة
  • مستمع: UpdateSitemap - يعيد توليد خريطة الموقع
  • مستمع: ClearCache - يمسح الذاكرة المؤقتة المرتبطة بالمنشور

نفذ الحدث والمستمعات والتسجيل ومنطق الإرسال.

تمرين 3: مشترك أحداث

أنشئ OrderEventSubscriber يتعامل مع أحداث متعددة متعلقة بالطلبات:

  • OrderCreated - إرسال بريد تأكيد، إنشاء فاتورة
  • OrderPaid - تحديث حالة الدفع، تحفيز الوفاء
  • OrderShipped - إرسال بريد تتبع، تحديث المخزون
  • OrderCancelled - استرداد الدفع، استعادة المخزون

نفذ المشترك مع جميع الطرق وسجله بشكل صحيح.

النقاط الرئيسية:
  • أحداث النموذج تطلق تلقائياً خلال دورة حياة النموذج
  • المراقبون يحافظون على معالجة الأحداث منظمة ومنفصلة عن النماذج
  • الأحداث المخصصة تسمح لك بتنفيذ أنظمة أحداث خاصة بالمجال
  • مشتركو الأحداث يجمعون معالجات الأحداث ذات الصلة معاً
  • استخدم مستمعات قائمة الانتظار للعمليات التي تستغرق وقتاً طويلاً
  • إرجاع false من أحداث "قبل" لإلغاء العملية