404 Not Found

404 Not Found


nginx

نظام الأحداث والبث في Laravel

نظام الأحداث هو "شبكة الاتصالات" في Laravel — يبث المرسل رسالة، ويتعامل كل مستقبل بها بشكل مستقل؛ لا يحتاج المرسل إلى معرفة من يستمع.

1. ما ستتعلمه


2. قصة حقيقية لمدير منتج

(1) مشكلة: يجب تحديث الصفحة يدويًا لرؤية تغييرات حالة الطلب

تدير Alice الطلبات في لوحة تحكم ShopMetrics — لا يمكنها رؤيتها فورًا بعد أن يقدم العملاء طلباتها وتضطر إلى تحديث الصفحة يدويًا كل 5 دقائق. Bob أسوأ حالًا؛ فهو يدير ثلاثة متاجر في نفس الوقت، ولا يستطيع مواكبة التحديث، وفاته خمسة طلبات عاجلة. قال Charlie: "نحن في 2024 وما زلنا نحدث يدويًا؟ هل سمعتم عن إشعارات WebSocket في الوقت الفعلي؟"

(2) حل بث الأحداث

بث أحداث Laravel — يتم تفعيل حدث عند إنشاء طلب؛ يدفع الخادم الحدث إلى الواجهة الأمامية عبر WebSocket، وتحدث صفحة Alice تلقائيًا بتأخير صفري.

PHP
// تقديم طلب → إرسال حدث → بث WebSocket
event(new OrderPlaced($order));
// متصفح Alice يستقبل الإشعار في الوقت الفعلي

(3) العائد

مع إشعارات Alice في الوقت الفعلي، تظهر الطلبات الجديدة على لوحة المعلومات خلال 0.5 ثانية، لذلك لن تفوتك طلبًا بعد الآن.


3. الأحداث والمستمعات

(1) إنشاء الأحداث والمستمعات

BASH
php artisan make:event OrderPlaced
php artisan make:listener SendOrderNotification --event=OrderPlaced
php artisan make:listener UpdateShopRevenue --event=OrderPlaced
php artisan make:listener SendOrderWebhook --event=OrderPlaced

(2) فئة الحدث

PHP
// app/Events/OrderPlaced.php
class OrderPlaced implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

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

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('tenant.' . $this->order->tenant_id),
            new PrivateChannel('shop.' . $this->order->shop_id),
        ];
    }

    public function broadcastWith(): array
    {
        return [
            'order_id' => $this->order->id,
            'order_number' => $this->order->order_number,
            'total' => (float) $this->order->total,
            'shop_name' => $this->order->shop->name,
            'customer_name' => $this->order->user->name,
        ];
    }

    public function broadcastAs(): string
    {
        return 'order.placed';
    }
}

(3) فئات المستمع

PHP
// app/Listeners/SendOrderNotification.php
class SendOrderNotification
{
    public function handle(OrderPlaced $event): void
    {
        $order = $event->order;

        // إرسال إشعار بريد إلكتروني لصاحب المتجر
        $order->shop->tenant->users()
            ->where('role', 'tenant_owner')
            ->each(fn ($user) => $user->notify(new OrderCreatedNotification($order)));
    }
}

// app/Listeners/UpdateShopRevenue.php
class UpdateShopRevenue
{
    public function handle(OrderPlaced $event): void
    {
        $event->order->shop->increment('revenue', $event->order->total);
    }
}

(4) تسجيل مستمع حدث

PHP
// app/Providers/EventServiceProvider.php
protected $listen = [
    OrderPlaced::class => [
        SendOrderNotification::class,
        UpdateShopRevenue::class,
        SendOrderWebhook::class,
    ],
    OrderStatusChanged::class => [
        SendStatusChangeNotification::class,
    ],
];

(1) ▶ مثال:تفعيل حدث طلب ShopMetrics

PHP
// app/Http/Controllers/OrderController.php
public function store(StoreOrderRequest $request): RedirectResponse
{
    $order = DB::transaction(function () use ($request) {
        $order = Order::create($request->validated());

        foreach ($request->items as $item) {
            $order->items()->create($item);
        }

        $order->updateTotal();

        return $order;
    });

    // إرسال حدث — يفعل جميع المستمعات + البث
    event(new OrderPlaced($order));

    return redirect()->route('orders.show', $order)
        ->with('success', 'Order placed!');
}

الناتج:

TEXT
// التنفيذ ناجح

4. جدولة الأحداث

(1) طريقة الجدولة

PHP
// الطريقة 1: مساعد event() (موصى به)
event(new OrderPlaced($order));

// الطريقة 2: Event facade
Event::dispatch(new OrderPlaced($order));

// الطريقة 3: إرسال ثابت على فئة الحدث
OrderPlaced::dispatch($order);

(2) مستمعات متزامنة مقابل غير متزامنة

PHP
// مستمع متزامن — يعمل في دورة الطلب
class UpdateShopRevenue implements ShouldHandleEventsAfterCommit
{
    public function handle(OrderPlaced $event): void
    {
        $event->order->shop->increment('revenue', $event->order->total);
    }
}

// مستمع غير متزامن — يُدفع إلى الطابور
class SendOrderWebhook implements ShouldQueue
{
    use InteractsWithQueue;

    public int $tries = 3;
    public int $backoff = 30;

    public function handle(OrderPlaced $event): void
    {
        Http::post($event->order->shop->webhook_url, [
            'event' => 'order.placed',
            'data' => new OrderResource($event->order),
        ]);
    }
}
النوع الواجهة طريقة التنفيذ السيناريوهات المناسبة
متزامن لا شيء تنفيذ متسلسل داخل الطلب تحديث قاعدة البيانات
غير متزامن ShouldQueue إدراج في الطابور للتنفيذ غير المتزامن إرسال بريد إلكتروني/Webhook
بعد المعاملة AfterCommit التنفيذ بعد الالتزام بالمعاملة يعتمد على البيانات المستمرة

(1) ▶ مثال:أحداث ShopMetrics وتسجيل المستمعات

PHP
// app/Providers/EventServiceProvider.php
class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        // أحداث الطلبات
        OrderPlaced::class => [
            UpdateShopRevenue::class,          // متزامن — تحديث الإحصائيات
            SendOrderNotification::class,      // غير متزامن — إرسال بريد إلكتروني
            SendOrderWebhook::class,           // غير متزامن — استدعاء webhook
        ],
        OrderStatusChanged::class => [
            SendStatusNotification::class,     // غير متزامن
            UpdateAnalyticsCache::class,       // متزامن — مسح التخزين المؤقت
        ],
        SubscriptionCreated::class => [
            SendWelcomeEmail::class,           // غير متزامن
            ProvisionTenantResources::class,   // غير متزامن
        ],
    ];
}

الناتج:

TEXT
// التنفيذ ناجح

5. آلية البث

(1) بنية البث

100%
sequenceDiagram
    participant S as خادم
    participant E as حدث
    participant B as مبث
    participant WS as خادم WebSocket
    participant C as عميل (Echo)

    S->>E: event(new OrderPlaced($order))
    E->>B: broadcastOn() → قنوات
    B->>WS: نشر إلى قناة Redis
    WS->>C: دفع عبر WebSocket
    C->>C: Echo يستقبل ويحدث واجهة المستخدم

(2) تكوين البث

BASH
# .env
BROADCAST_CONNECTION=redis
QUEUE_CONNECTION=redis

# تثبيت التبعيات
composer require pusher/pusher-php-server
# أو لـ Redis:
# predis/predis مثبت بالفعل
PHP
// config/broadcasting.php
'default' => env('BROADCAST_CONNECTION', 'redis'),

'connections' => [
    'pusher' => [
        'driver' => 'pusher',
        'key' => env('PUSHER_APP_KEY'),
        'secret' => env('PUSHER_APP_SECRET'),
        'app_id' => env('PUSHER_APP_ID'),
    ],
    'redis' => [
        'driver' => 'redis',
        'connection' => 'default',
    ],
],

(3) مقارنة مشغلات البث

المشغلات الخدمات مستضاف ذاتيًا الأداء التكلفة
Pusher Pusher Cloud عالي مدفوع
Redis Redis + Laravel Echo Server عالي مجاني
Ably Ably Cloud عالي مدفوع
Log سجل (للتطوير) مجاني

(1) ▶ مثال:تعريف أحداث بث ShopMetrics

PHP
// app/Events/OrderStatusChanged.php
class OrderStatusChanged implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public Order $order,
        public string $oldStatus,
        public string $newStatus,
    ) {}

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('tenant.' . $this->order->tenant_id),
        ];
    }

    public function broadcastWith(): array
    {
        return [
            'order_id' => $this->order->id,
            'order_number' => $this->order->order_number,
            'old_status' => $this->oldStatus,
            'new_status' => $this->newStatus,
            'updated_at' => $this->order->updated_at->toISOString(),
        ];
    }

    public function broadcastAs(): string
    {
        return 'order.status_changed';
    }
}

الناتج:

TEXT
// التنفيذ ناجح

6. أنواع القنوات والتصريح

(1) ثلاثة أنواع من القنوات

القناة البادئة الرؤية الغرض
عامة channel- الجميع الإعلانات، إشعارات على مستوى الموقع
خاصة private- مستخدم مصرح له حصري للمستأجر/المستخدم
حضور presence- تصريح + قائمة متصلين التعاون، الدردشة

(2) تصريح القناة

PHP
// routes/channels.php
use Illuminate\Support\Facades\Broadcast;

// قناة خاصة — فقط أعضاء المستأجر يمكنهم الاستماع
Broadcast::channel('tenant.{tenantId}', function ($user, $tenantId) {
    return $user->tenant_id === (int) $tenantId;
});

// قناة خاصة — فقط صاحب/محلل المتجر
Broadcast::channel('shop.{shopId}', function ($user, $shopId) {
    $shop = Shop::find($shopId);
    return $shop && $user->tenant_id === $shop->tenant_id;
});

// قناة حضور — من متصل
Broadcast::channel('shop.dashboard.{shopId}', function ($user, $shopId) {
    if ($user->tenant_id === Shop::find($shopId)?->tenant_id) {
        return ['id' => $user->id, 'name' => $user->name, 'role' => $user->role];
    }
});

(1) ▶ مثال:تصريح قنوات ShopMetrics

PHP
// routes/channels.php
Broadcast::channel('tenant.{tenantId}', function ($user, $tenantId) {
    return $user->tenant_id === (int) $tenantId
        && $user->tenant->status === 'active';
});

Broadcast::channel('shop.{shopId}', function ($user, $shopId) {
    $shop = Shop::find($shopId);
    if (!$shop || $user->tenant_id !== $shop->tenant_id) {
        return false;
    }
    return ['id' => $user->id, 'name' => $user->name];
});

Broadcast::channel('notifications.{userId}', function ($user, $userId) {
    return (int) $user->id === (int) $userId;
});

الناتج:

TEXT
// التنفيذ ناجح

7. الاستقبال في الواجهة الأمامية

(1) تثبيت Laravel Echo

BASH
npm install laravel-echo pusher-js
# أو لـ Redis:
npm install laravel-echo-connector socket.io-client

(2) تكوين Echo

JAVASCRIPT
// resources/js/app.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
    wsHost: import.meta.env.VITE_PUSHER_HOST,
    wsPort: import.meta.env.VITE_PUSHER_PORT ?? 6001,
    forceTLS: false,
    enabledTransports: ['ws'],
});

(3) الاستماع للأحداث

JAVASCRIPT
// الاستماع على قناة خاصة — أحداث خاصة بالمستأجر
window.Echo.private(`tenant.${tenantId}`)
    .listen('.order.placed', (e) => {
        showToast(`طلب جديد: ${e.order_number} — $${e.total}`);
        updateOrdersList(e);
    })
    .listen('.order.status_changed', (e) => {
        updateOrderStatus(e.order_id, e.new_status);
    });

// الاستماع على قناة حضور — معرفة من متصل
window.Echo.join(`shop.dashboard.${shopId}`)
    .here((users) => {
        updateOnlineUsers(users);
    })
    .joining((user) => {
        addOnlineUser(user);
    })
    .leaving((user) => {
        removeOnlineUser(user);
    });

(1) ▶ مثال:لوحة معلومات ShopMetrics في الوقت الفعلي

HTML
<!-- resources/views/dashboard/index.blade.php -->
<script>
    const tenantId = {{ auth()->user()->tenant_id }};

    // تهيئة Echo
    window.Echo.private(`tenant.${tenantId}`)
        .listen('.order.placed', (event) => {
            // تحديث الإحصائيات
            const stats = document.getElementById('stats');
            const orderCount = stats.querySelector('.order-count');
            orderCount.textContent = parseInt(orderCount.textContent) + 1;

            // إضافة إلى الطلبات الأخيرة
            const list = document.getElementById('recent-orders');
            list.insertAdjacentHTML('afterbegin', `
                <tr class="bg-green-50">
                    <td>${event.order_number}</td>
                    <td>${event.shop_name}</td>
                    <td>$${event.total.toFixed(2)}</td>
                    <td><span class="badge-blue">جديد</span></td>
                </tr>
            `);

            // عرض الإشعار
            showNotification(`طلب جديد من ${event.customer_name}: $${event.total}`);
        })
        .listen('.order.status_changed', (event) => {
            const row = document.querySelector(`[data-order="${event.order_id}"]`);
            if (row) {
                row.querySelector('.status-badge').textContent = event.new_status;
                row.querySelector('.status-badge').className = `status-badge badge-${event.new_status}`;
            }
        });
</script>

الناتج:

TEXT
// التنفيذ ناجح

8. مثال شامل: نظام إشعارات ShopMetrics في الوقت الفعلي

PHP
// ============================================
// شامل: إشعارات ShopMetrics في الوقت الفعلي
// يغطي: الأحداث، المستمعات، البث، القنوات، Echo
// ============================================

// app/Events/OrderPlaced.php
class OrderPlaced implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(public Order $order) {}

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('tenant.' . $this->order->tenant_id),
            new PrivateChannel('shop.' . $this->order->shop_id),
        ];
    }

    public function broadcastWith(): array
    {
        $this->order->load('shop', 'user');
        return [
            'order_id' => $this->order->id,
            'order_number' => $this->order->order_number,
            'total' => (float) $this->order->total,
            'status' => $this->order->status,
            'shop' => ['id' => $this->order->shop->id, 'name' => $this->order->shop->name],
            'customer' => ['id' => $this->order->user->id, 'name' => $this->order->user->name],
            'created_at' => $this->order->created_at->toISOString(),
        ];
    }

    public function broadcastAs(): string { return 'order.placed'; }
}

// app/Events/OrderStatusChanged.php
class OrderStatusChanged implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public Order $order,
        public string $oldStatus,
        public string $newStatus,
    ) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('tenant.' . $this->order->tenant_id)];
    }

    public function broadcastWith(): array
    {
        return [
            'order_id' => $this->order->id,
            'order_number' => $this->order->order_number,
            'old_status' => $this->oldStatus,
            'new_status' => $this->newStatus,
        ];
    }

    public function broadcastAs(): string { return 'order.status_changed'; }
}

// التفعيل في فئة الخدمة
class OrderService
{
    public function place(array $data): Order
    {
        $order = DB::transaction(function () use ($data) {
            $order = Order::create($data);
            // ... إنشاء العناصر، حساب الإجمالي
            return $order;
        });

        event(new OrderPlaced($order));
        return $order;
    }

    public function changeStatus(Order $order, string $newStatus): Order
    {
        $oldStatus = $order->status;
        $order->update(['status' => $newStatus]);
        event(new OrderStatusChanged($order, $oldStatus, $newStatus));
        return $order;
    }
}

❓ أسئلة شائعة

س ما الفرق بين الأحداث والاستدعاءات المباشرة؟
ج الاستدعاء المباشر (A→B) مقترن بإحكام؛ A يجب أن يكون على علم بوجود B. الحدث (A→Event→B) مقترن بشكل فضفاض؛ A لا يعرف من يستمع. يمكن إضافة ميزات جديدة ببساطة بإضافة مستمعات، دون تعديل كود المرسل.
س متى يجب استخدام المستمع ShouldQueue؟
ج استخدم المستمعات غير المتزامنة للعمليات التي تستغرق وقتًا (إرسال بريد إلكتروني، تفعيل webhooks، توليد تقارير)؛ استخدم المستمعات المتزامنة للعمليات الفورية (تحديث قاعدة البيانات، مسح التخزين المؤقت). المستمعات غير المتزامنة لا تحظر الطلبات.
س كيف أختار بين Pusher وRedis للإشعارات الفورية؟
ج استخدم Pusher (خدمة مستضافة) للنشرات صغيرة النطاق أو الإطلاقات السريعة؛ استخدم Redis + Laravel Echo Server (مستضاف ذاتيًا) للنشرات كبيرة النطاق أو عندما تكون التكلفة مصدر قلق. Pusher له حصص مجانية محدودة، بينما تكلفة خادم Redis ثابتة.
س ماذا أفعل إذا لم تستقبل الواجهة الأمامية أحداث البث؟
ج قائمة الفحص: 1) QUEUE_CONNECTION ليس مضبوطًا على "sync" 2) queue:work يعمل 3) المفتاح/المضيف المكون في Echo صحيح 4) تصريح القناة يعيد true 5) حدث البث ينفذ ShouldBroadcast.
س ماذا أفعل إذا كانت بيانات حدث البث كبيرة جدًا؟
ج استخدم broadcastWith() لإرسال الحقول الضرورية فقط (ID + معلومات رئيسية)؛ بمجرد استلام الواجهة الأمامية، يمكنها استخدام AJAX لجلب البيانات الكاملة. تجنب تضمين عدد كبير من الموارد المرتبطة داخل بيانات البث.
س كيف أختبر مستمعات الأحداث؟
ج استخدم Event::fake() لمحاكاة إرسال الأحداث، وأكد أن الحدث تم إرساله: Event::assertDispatched(OrderPlaced::class). اكتب اختبارات وحدة منفصلة للمستمعات.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ حدث OrderPlaced ومستمع SendOrderNotification، الذي يُفعل عند إنشاء طلب. استخدم Event::fake() لكتابة اختبار للتحقق من إرسال الحدث.

  2. تمرين متقدم (⭐⭐): نفذ بث ShouldBroadcast لحدث OrderPlaced، وقم بتكوين مشغل بث Redis، واستخدم Laravel Echo في الواجهة الأمامية للاستماع إلى قناة private-tenant.{id} لعرض إشعارات فورية للطلبات الجديدة.

  3. تحدي (⭐⭐⭐): نفذ لوحة معلومات تعاونية متعددة المستخدمين باستخدام قناة الحضور — عندما يشاهد عدة مستخدمين بيانات نفس المتجر في نفس الوقت، اعرض قائمة المستخدمين المتصلين؛ عندما يعدل مستخدم واحد البيانات، يرى المستخدمون الآخرون التغييرات في الوقت الفعلي (بث تغييرات حالة الطلب).

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%