Eloquent المتقدم: الأحداث والمراقبين
فهم أحداث 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}");
});
}
}
// 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من أحداث "قبل" لإلغاء العملية