إطار Laravel

بناء ميزات الوقت الفعلي مع البث

18 دقيقة الدرس 32 من 45

مقدمة إلى البث

يتيح لك البث في Laravel بث الأحداث من جانب الخادم إلى تطبيق JavaScript من جانب العميل باستخدام WebSockets. يتيح ذلك ميزات الوقت الفعلي مثل الإشعارات والرسائل الدردشة والتحديثات المباشرة والتحرير التعاوني دون الحاجة إلى الاستطلاع أو طلبات HTTP المستمرة.

حالات استخدام الوقت الفعلي:

  • الإشعارات المباشرة (رسائل جديدة، طلبات، تنبيهات)
  • تطبيقات الدردشة وأنظمة المراسلة
  • لوحات المعلومات والتحليلات في الوقت الفعلي
  • التحرير التعاوني (المستندات، اللوحات البيضاء)
  • التعليقات والتفاعلات المباشرة
  • تتبع الحضور (من متصل)
  • الخلاصات المباشرة وتدفقات الأنشطة

بنية البث

يتكون نظام البث في Laravel من عدة مكونات:

  • الأحداث: فئات PHP تمثل شيئاً حدث في تطبيقك
  • قنوات البث: مسارات مسماة حيث يتم بث الأحداث (عامة، خاصة، حضور)
  • عمال الصف: معالجة وظائف البث بشكل غير متزامن
  • برنامج تشغيل البث: خدمة تتعامل مع اتصالات WebSocket (Pusher، Ably، Redis، Soketi)
  • Laravel Echo: مكتبة JavaScript تستمع لأحداث البث على جانب العميل

إعداد البث

أولاً، قم بتكوين برنامج تشغيل البث في ملف .env:

# .env - استخدام Pusher
BROADCAST_DRIVER=pusher

PUSHER_APP_ID=your-app-id
PUSHER_APP_KEY=your-app-key
PUSHER_APP_SECRET=your-app-secret
PUSHER_APP_CLUSTER=mt1

# أو استخدام Redis (يتطلب حزمة predis/predis)
BROADCAST_DRIVER=redis

قم بتثبيت الحزم المطلوبة:

# تثبيت Pusher PHP SDK
composer require pusher/pusher-php-server

# تثبيت Laravel Echo و Pusher JS (الواجهة الأمامية)
npm install --save-dev laravel-echo pusher-js

# للبث عبر Redis
composer require predis/predis

قم بتمكين البث في config/app.php عن طريق إلغاء التعليق على BroadcastServiceProvider:

// config/app.php
'providers' => [
    // ...
    App\Providers\BroadcastServiceProvider::class,
],

Soketi - بديل مجاني: Soketi هو بديل مفتوح المصدر ومجاني لـ Pusher يمكنك استضافته بنفسك. إنه مثالي للتطوير والإنتاج دون رسوم شهرية. قم بالتثبيت بـ: npm install -g @soketi/soketi وقم بالتشغيل بـ: soketi start

إنشاء أحداث البث

أنشئ حدثاً ينفذ واجهة ShouldBroadcast:

php artisan make:event OrderShipped
<?php
// app/Events/OrderShipped.php
namespace App\Events;

use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderShipped implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    // تحديد القناة (القنوات) للبث عليها
    public function broadcastOn()
    {
        // قناة عامة - يمكن لأي شخص الاستماع
        return new Channel('orders');

        // قناة خاصة - تتطلب المصادقة
        // return new PrivateChannel('orders.' . $this->order->user_id);

        // قناة حضور - تتبع من يستمع
        // return new PresenceChannel('orders');
    }

    // تخصيص اسم الحدث (اختياري)
    public function broadcastAs()
    {
        return 'order.shipped';
    }

    // تخصيص البيانات المبثوثة (اختياري)
    public function broadcastWith()
    {
        return [
            'order_id' => $this->order->id,
            'tracking_number' => $this->order->tracking_number,
            'status' => $this->order->status,
            'shipped_at' => $this->order->shipped_at,
        ];
    }
}

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

<?php
// app/Http/Controllers/OrderController.php
namespace App\Http\Controllers;

use App\Models\Order;
use App\Events\OrderShipped;

class OrderController extends Controller
{
    public function ship(Order $order)
    {
        $order->update(['status' => 'shipped', 'shipped_at' => now()]);

        // بث الحدث
        event(new OrderShipped($order));

        // أو استخدام مساعد broadcast()
        // broadcast(new OrderShipped($order));

        return response()->json(['message' => 'تم شحن الطلب بنجاح']);
    }
}

أنواع القنوات

يدعم Laravel ثلاثة أنواع من قنوات البث:

1. القنوات العامة

يمكن لأي شخص الاستماع إلى القنوات العامة دون مصادقة:

public function broadcastOn()
{
    return new Channel('public-updates');
}

2. القنوات الخاصة

تتطلب القنوات الخاصة التفويض. حدد منطق التفويض في routes/channels.php:

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

// قناة خاصة بالمستخدم
Broadcast::channel('orders.{userId}', function ($user, $userId) {
    return (int) $user->id === (int) $userId;
});

// قناة خاصة بالطلب
Broadcast::channel('order.{orderId}', function ($user, $orderId) {
    return $user->orders()->where('id', $orderId)->exists();
});

// قناة خاصة بالمشرفين فقط
Broadcast::channel('admin', function ($user) {
    return $user->is_admin;
});
// استخدام قناة خاصة في الحدث
public function broadcastOn()
{
    return new PrivateChannel('orders.' . $this->order->user_id);
}

3. قنوات الحضور

تتبع قنوات الحضور من المشترك حالياً في القناة:

<?php
// routes/channels.php
Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
    if ($user->canJoinRoom($roomId)) {
        return [
            'id' => $user->id,
            'name' => $user->name,
            'avatar' => $user->avatar_url,
        ];
    }
});
// استخدام قناة الحضور في الحدث
public function broadcastOn()
{
    return new PresenceChannel('chat.1');
}

مهم: تأكد من تشغيل عامل الصف لمعالجة أحداث البث: php artisan queue:work. بدون عامل الصف، سيتم معالجة أحداث البث بشكل متزامن وقد يؤدي ذلك إلى إبطاء تطبيقك.

إعداد Laravel Echo (الواجهة الأمامية)

قم بتكوين Laravel Echo في JavaScript الخاص بك للاستماع للأحداث:

// resources/js/bootstrap.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,
    forceTLS: true,

    // للقنوات المصادق عليها
    authEndpoint: '/broadcasting/auth',
    auth: {
        headers: {
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
        }
    }
});

// لـ Soketi (التطوير المحلي)
/*
window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'app-key',
    wsHost: 'localhost',
    wsPort: 6001,
    forceTLS: false,
    disableStats: true,
    enabledTransports: ['ws', 'wss'],
});
*/
// إضافة إلى .env
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

الاستماع للأحداث

استمع لأحداث البث في JavaScript الخاص بك:

// الاستماع إلى قناة عامة
Echo.channel('orders')
    .listen('OrderShipped', (e) => {
        console.log('تم شحن الطلب:', e.order_id);
        showNotification('تم شحن طلبك #' + e.order_id + '!');
    });

// الاستماع إلى قناة خاصة (يتطلب المصادقة)
Echo.private('orders.' + userId)
    .listen('.order.shipped', (e) => {
        console.log('تم شحن طلبك:', e);
        updateOrderStatus(e.order_id, 'shipped');
    });

// الاستماع إلى قناة الحضور
Echo.join('chat.1')
    .here((users) => {
        // المستخدمون الموجودون حالياً في القناة
        console.log('المستخدمون المتصلون:', users);
        displayOnlineUsers(users);
    })
    .joining((user) => {
        // انضم مستخدم إلى القناة
        console.log(user.name + ' انضم');
        addOnlineUser(user);
    })
    .leaving((user) => {
        // غادر مستخدم القناة
        console.log(user.name + ' غادر');
        removeOnlineUser(user);
    })
    .listen('MessageSent', (e) => {
        // تم استلام رسالة دردشة
        displayMessage(e.message);
    });

تسمية الأحداث: عند استخدام broadcastAs()، قم بإضافة نقطة قبل اسم الحدث عند الاستماع: .listen('.order.shipped'). بدون broadcastAs()، استخدم اسم الفئة: .listen('OrderShipped')

إشعارات الوقت الفعلي

يجعل Laravel من السهل إرسال إشعارات البث:

<?php
// app/Notifications/InvoicePaid.php
namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;

class InvoicePaid extends Notification implements ShouldQueue
{
    use Queueable;

    public $invoice;

    public function __construct($invoice)
    {
        $this->invoice = $invoice;
    }

    public function via($notifiable)
    {
        return ['database', 'broadcast'];
    }

    public function toArray($notifiable)
    {
        return [
            'invoice_id' => $this->invoice->id,
            'amount' => $this->invoice->amount,
            'message' => 'تم دفع الفاتورة #' . $this->invoice->id,
        ];
    }

    public function toBroadcast($notifiable)
    {
        return new BroadcastMessage([
            'invoice_id' => $this->invoice->id,
            'amount' => $this->invoice->amount,
            'message' => 'تم دفع الفاتورة #' . $this->invoice->id,
        ]);
    }
}
// إرسال إشعار
$user->notify(new InvoicePaid($invoice));

// الاستماع للإشعارات في الواجهة الأمامية
Echo.private('App.Models.User.' + userId)
    .notification((notification) => {
        console.log('تم استلام إشعار:', notification);
        showNotification(notification.message);
        updateNotificationBadge();
    });

بناء دردشة في الوقت الفعلي

إليك مثال كامل لنظام دردشة في الوقت الفعلي:

<?php
// app/Events/MessageSent.php
namespace App\Events;

use App\Models\Message;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MessageSent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $message;
    public $user;

    public function __construct(Message $message, User $user)
    {
        $this->message = $message;
        $this->user = $user;
    }

    public function broadcastOn()
    {
        return new PresenceChannel('chat.' . $this->message->chat_room_id);
    }

    public function broadcastWith()
    {
        return [
            'id' => $this->message->id,
            'body' => $this->message->body,
            'created_at' => $this->message->created_at->toDateTimeString(),
            'user' => [
                'id' => $this->user->id,
                'name' => $this->user->name,
                'avatar' => $this->user->avatar_url,
            ],
        ];
    }
}
<?php
// app/Http/Controllers/ChatController.php
namespace App\Http\Controllers;

use App\Models\Message;
use App\Events\MessageSent;
use Illuminate\Http\Request;

class ChatController extends Controller
{
    public function sendMessage(Request $request)
    {
        $validated = $request->validate([
            'chat_room_id' => 'required|exists:chat_rooms,id',
            'body' => 'required|string|max:1000',
        ]);

        $message = Message::create([
            'user_id' => auth()->id(),
            'chat_room_id' => $validated['chat_room_id'],
            'body' => $validated['body'],
        ]);

        broadcast(new MessageSent($message, auth()->user()))->toOthers();

        return response()->json($message);
    }
}
// JavaScript للواجهة الأمامية
const chatRoomId = 1;

// الانضمام إلى غرفة الدردشة
Echo.join(`chat.${chatRoomId}`)
    .here((users) => {
        displayOnlineUsers(users);
    })
    .joining((user) => {
        addOnlineUser(user);
        showSystemMessage(`${user.name} انضم إلى الدردشة`);
    })
    .leaving((user) => {
        removeOnlineUser(user);
        showSystemMessage(`${user.name} غادر الدردشة`);
    })
    .listen('MessageSent', (e) => {
        appendMessage(e);
    });

// إرسال رسالة
function sendMessage(body) {
    axios.post('/chat/message', {
        chat_room_id: chatRoomId,
        body: body
    }).then(response => {
        appendMessage(response.data);
        clearInput();
    });
}

function appendMessage(message) {
    const messageHtml = `
        <div class="message">
            <img src="${message.user.avatar}" alt="${message.user.name}">
            <div>
                <strong>${message.user.name}</strong>
                <p>${message.body}</p>
                <span>${message.created_at}</span>
            </div>
        </div>
    `;
    document.querySelector('#messages').insertAdjacentHTML('beforeend', messageHtml);
}

نصيحة أداء: استخدم دالة toOthers() عند البث لمنع المرسل من تلقي رسالته مرتين: broadcast(new MessageSent($message))->toOthers()

تمرين تطبيقي 1: نظام إشعارات الوقت الفعلي

ابنِ نظام إشعارات في الوقت الفعلي بالميزات التالية:

  • أنشئ حدث بث NewNotification
  • أرسل إشعارات عندما يتلقى المستخدمون رسائل أو متابعات أو إعجابات جديدة
  • اعرض الإشعارات في قائمة منسدلة بأيقونة جرس (مع عداد)
  • ضع علامة على الإشعارات كمقروءة عند النقر عليها
  • استخدم قنوات خاصة للإشعارات الخاصة بالمستخدم
  • احفظ الإشعارات في قاعدة البيانات باستخدام نظام الإشعارات في Laravel

تلميح: استخدم Echo.private('App.Models.User.' + userId).notification() للاستماع للإشعارات.

تمرين تطبيقي 2: تحديثات لوحة المعلومات المباشرة

أنشئ لوحة معلومات مباشرة تتحدث في الوقت الفعلي:

  • اعرض المقاييس: إجمالي الطلبات، الإيرادات، المستخدمون النشطون
  • ابث حدث DashboardUpdated عند تغيير المقاييس
  • حدِّث لوحة المعلومات دون إعادة تحميل الصفحة عند تلقي الحدث
  • أضف رسم بياني يتحدث في الوقت الفعلي (استخدم Chart.js)
  • أظهر مؤشر "مباشر" عند الاتصال بـ WebSocket
  • تعامل مع إعادة الاتصال عند فقدان الاتصال

إضافة: أضف قناة حضور لإظهار المشرفين الآخرين الذين يشاهدون لوحة المعلومات.

تمرين تطبيقي 3: تحرير المستندات التعاوني

ابنِ محرر تعاوني بسيط:

  • أنشئ حدث DocumentUpdated يبث التغييرات
  • أظهر مواضع المؤشرات للمستخدمين الآخرين (قناة الحضور)
  • ابث تغييرات النص مع التأخير (انتظر 500 مللي ثانية بعد توقف الكتابة)
  • اعرض من يحرر المستند حالياً
  • تعامل مع النزاعات عند تحرير عدة مستخدمين في وقت واحد
  • احفظ المستند في قاعدة البيانات بشكل دوري

تحدي: نفذ التحويل التشغيلي أو CRDT لحل النزاعات.

الخلاصة

في هذا الدرس، تعلمت كيفية بناء ميزات الوقت الفعلي في Laravel باستخدام البث. المفاهيم الرئيسية تشمل:

  • فهم بنية البث واتصال WebSocket
  • إعداد البث مع Pusher أو Soketi أو Redis
  • إنشاء أحداث البث التي تنفذ ShouldBroadcast
  • العمل مع القنوات العامة والخاصة وقنوات الحضور
  • تفويض القنوات الخاصة في routes/channels.php
  • تكوين Laravel Echo في الواجهة الأمامية
  • الاستماع للأحداث والتعامل مع التحديثات في الوقت الفعلي
  • بناء الإشعارات والدردشة ولوحات المعلومات في الوقت الفعلي
  • تتبع الحضور عبر الإنترنت مع قنوات الحضور

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