الأحداث والمستمعين في Laravel
يوفر نظام الأحداث في Laravel تطبيقًا بسيطًا لنمط المراقب، مما يسمح لك بالاشتراك والاستماع إلى أحداث متنوعة في تطبيقك. توفر الأحداث طريقة رائعة لفصل جوانب مختلفة من تطبيقك، مما يجعل الكود أكثر قابلية للصيانة والاختبار.
البنية المدفوعة بالأحداث
تسمح لك الأحداث ببناء أنظمة مفصولة بشكل فضفاض حيث يمكن لأجزاء مختلفة من تطبيقك الاستجابة للإجراءات دون الاعتماد المباشر على بعضها البعض:
فوائد الأحداث:
- الفصل: المكونات لا تحتاج إلى معرفة بعضها البعض
- قابلية الصيانة: من السهل إضافة/إزالة الوظائف دون لمس الكود الموجود
- قابلية الاختبار: يمكن اختبار كل مستمع بشكل مستقل
- إعادة الاستخدام: يمكن لمستمعين متعددين الاستجابة لنفس الحدث
- المعالجة غير المتزامنة: يمكن وضع المستمعين في قائمة انتظار للتنفيذ في الخلفية
إنشاء الأحداث
قم بإنشاء فئة حدث باستخدام Artisan:
php artisan make:event OrderShipped
php artisan make:event UserRegistered
php artisan make:event PaymentProcessed
مثال على فئة حدث:
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
}
حدث مع سياق إضافي:
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $user;
public $verificationUrl;
public $source;
public function __construct(User $user, string $verificationUrl, string $source = 'web')
{
$this->user = $user;
$this->verificationUrl = $verificationUrl;
$this->source = $source;
}
// البث على هذه القنوات
public function broadcastOn()
{
return new PrivateChannel('admin');
}
// تخصيص بيانات البث
public function broadcastWith()
{
return [
'user_id' => $this->user->id,
'user_name' => $this->user->name,
'registered_at' => $this->user->created_at->toISOString(),
];
}
}
إنشاء المستمعين
قم بإنشاء فئات المستمعين باستخدام Artisan:
php artisan make:listener SendShipmentNotification --event=OrderShipped
php artisan make:listener UpdateInventory --event=OrderShipped
php artisan make:listener SendWelcomeEmail --event=UserRegistered
مثال على فئة مستمع:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use App\Notifications\ShipmentNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
// عدد مرات المحاولة
public $tries = 3;
// المهلة بالثواني
public $timeout = 60;
// اسم قائمة الانتظار
public $queue = 'notifications';
public function __construct()
{
//
}
public function handle(OrderShipped $event)
{
$order = $event->order;
// إرسال إشعار للعميل
$order->customer->notify(
new ShipmentNotification($order)
);
// تسجيل الشحنة
\Log::info('تم إرسال إشعار الشحن', [
'order_id' => $order->id,
'customer_id' => $order->customer_id,
]);
}
// معالجة فشل المهمة
public function failed(OrderShipped $event, $exception)
{
\Log::error('فشل إرسال إشعار الشحن', [
'order_id' => $event->order->id,
'error' => $exception->getMessage(),
]);
}
}
مستمع مع منطق شرطي:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
class UpdateInventory
{
public function handle(OrderShipped $event)
{
$order = $event->order;
foreach ($order->items as $item) {
$product = $item->product;
// تقليل المخزون
$product->decrement('stock', $item->quantity);
// فحص إذا كان المخزون منخفضًا
if ($product->stock < $product->low_stock_threshold) {
event(new LowStockAlert($product));
}
// فحص إذا نفد المخزون
if ($product->stock <= 0) {
$product->update(['is_available' => false]);
event(new OutOfStockAlert($product));
}
}
}
// إيقاف الانتشار إذا لزم الأمر
public function shouldQueue(OrderShipped $event)
{
return $event->order->status === 'shipped';
}
}
تسجيل الأحداث والمستمعين
سجل الأحداث والمستمعين في EventServiceProvider:
<?php
namespace App\Providers;
use App\Events\OrderShipped;
use App\Events\UserRegistered;
use App\Listeners\SendShipmentNotification;
use App\Listeners\UpdateInventory;
use App\Listeners\SendWelcomeEmail;
use App\Listeners\CreateUserProfile;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
// أحداث الطلبات
OrderShipped::class => [
SendShipmentNotification::class,
UpdateInventory::class,
],
// أحداث المستخدم
UserRegistered::class => [
SendWelcomeEmail::class,
CreateUserProfile::class,
],
// أحداث Laravel المدمجة
'Illuminate\Auth\Events\Login' => [
'App\Listeners\LogSuccessfulLogin',
],
];
public function boot()
{
//
}
}
نصيحة: بعد إضافة تعيينات الحدث-المستمع، قم بتشغيل php artisan event:list لرؤية جميع الأحداث المسجلة ومستمعيها.
إرسال الأحداث
أرسل الأحداث باستخدام طرق متعددة:
use App\Events\OrderShipped;
// باستخدام مساعد event()
event(new OrderShipped($order));
// باستخدام واجهة Event
Event::dispatch(new OrderShipped($order));
// باستخدام trait Dispatchable
OrderShipped::dispatch($order);
// إرسال إذا كان الشرط صحيحًا
OrderShipped::dispatchIf($order->status === 'shipped', $order);
// إرسال ما لم يكن الشرط صحيحًا
OrderShipped::dispatchUnless($order->status === 'cancelled', $order);
إرسال الأحداث من النماذج باستخدام أحداث النموذج:
<?php
namespace App\Models;
use App\Events\OrderShipped;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
protected $dispatchesEvents = [
'created' => OrderCreated::class,
'updated' => OrderUpdated::class,
'deleted' => OrderDeleted::class,
];
// أو استخدم طريقة boot
protected static function boot()
{
parent::boot();
static::updated(function ($order) {
if ($order->isDirty('status') && $order->status === 'shipped') {
event(new OrderShipped($order));
}
});
}
}
مشتركو الأحداث
يسمح لك مشتركو الأحداث بالاشتراك في أحداث متعددة داخل فئة واحدة:
php artisan make:listener UserEventSubscriber
مثال على فئة مشترك:
<?php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Events\UserLoggedIn;
use App\Events\UserPasswordChanged;
use Illuminate\Events\Dispatcher;
class UserEventSubscriber
{
public function handleUserRegistration($event)
{
// إرسال بريد ترحيب
$event->user->sendWelcomeEmail();
// إنشاء تفضيلات افتراضية
$event->user->preferences()->create([
'theme' => 'light',
'notifications_enabled' => true,
]);
}
public function handleUserLogin($event)
{
// تحديث طابع الوقت لآخر تسجيل دخول
$event->user->update([
'last_login_at' => now(),
'last_login_ip' => request()->ip(),
]);
// تسجيل النشاط
activity('user-login')
->by($event->user)
->log('قام المستخدم بتسجيل الدخول');
}
public function handlePasswordChange($event)
{
// إرسال إشعار أمني
$event->user->notify(new PasswordChangedNotification());
// إبطال الجلسات الأخرى
$event->user->invalidateOtherSessions();
}
public function subscribe(Dispatcher $events)
{
$events->listen(
UserRegistered::class,
[UserEventSubscriber::class, 'handleUserRegistration']
);
$events->listen(
UserLoggedIn::class,
[UserEventSubscriber::class, 'handleUserLogin']
);
$events->listen(
UserPasswordChanged::class,
[UserEventSubscriber::class, 'handlePasswordChange']
);
}
}
سجل المشترك في EventServiceProvider:
protected $subscribe = [
UserEventSubscriber::class,
OrderEventSubscriber::class,
];
مستمعو الأحداث في قائمة الانتظار
اجعل المستمعين قابلين للوضع في قائمة الانتظار لأداء أفضل:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
public function handle(OrderShipped $event)
{
// سيتم تشغيل هذا في الخلفية
$event->order->customer->notify(
new ShipmentNotification($event->order)
);
}
// تحديد يدويًا ما إذا كان يجب وضعه في قائمة الانتظار
public function shouldQueue(OrderShipped $event)
{
return $event->order->customer->prefers_email;
}
// تحديد اتصال قائمة الانتظار
public function viaConnection()
{
return 'redis';
}
// تحديد اسم قائمة الانتظار
public function viaQueue()
{
return 'notifications';
}
}
تحذير: عند استخدام مستمعين في قائمة الانتظار، تأكد من أن جميع البيانات الممررة في الحدث قابلة للتسلسل. تجنب تمرير الإغلاقات أو الموارد (مثل اتصالات قاعدة البيانات) في الأحداث.
إيقاف انتشار الحدث
أوقف انتشار الحدث عن طريق إرجاع false من مستمع:
public function handle(OrderShipped $event)
{
// افعل شيئًا
if ($event->order->is_express_shipping) {
// إيقاف تنفيذ المستمعين الآخرين
return false;
}
}
تمرين 1: أنشئ حدث "ProductViewed" ونظام مستمع. يجب أن يتتبع الحدث مشاهدات المنتج، ويحدث أعداد المشاهدة، ويوصي بمنتجات ذات صلة. أنشئ على الأقل 3 مستمعين لاهتمامات مختلفة.
تمرين 2: ابنِ مشترك أحداث لأحداث منشورات المدونة (تم الإنشاء، تم التحديث، تم النشر، تم الحذف). يجب أن يطلق كل حدث إجراءات مناسبة مثل مسح الذاكرة المؤقتة وإرسال الإشعارات وتسجيل النشاط.
تمرين 3: أنشئ نظام إشعارات في الوقت الفعلي باستخدام أحداث مبثوثة. عندما يتلقى مستخدم رسالة، أرسل حدثًا يبث إلى قناته الخاصة ويحدث واجهة المستخدم في الوقت الفعلي.
اختبار الأحداث
اختبر الأحداث والمستمعين باستخدام أدوات اختبار Laravel:
use Illuminate\Support\Facades\Event;
use App\Events\OrderShipped;
public function test_order_shipped_event_is_dispatched()
{
Event::fake();
$order = Order::factory()->create(['status' => 'processing']);
$order->update(['status' => 'shipped']);
Event::assertDispatched(OrderShipped::class, function ($event) use ($order) {
return $event->order->id === $order->id;
});
}
public function test_listeners_are_called()
{
Event::fake([OrderShipped::class]);
event(new OrderShipped($order));
Event::assertListening(
OrderShipped::class,
SendShipmentNotification::class
);
}
توفر الأحداث والمستمعون طريقة قوية لبناء تطبيقات مفصولة وقابلة للصيانة. في الدرس التالي، سنستكشف قوائم الانتظار ومعالجة المهام للتعامل مع المهام التي تستغرق وقتًا طويلاً في الخلفية.