إطار Laravel

Laravel Reverb و WebSockets

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

Laravel Reverb و WebSockets

Laravel Reverb هو خادم WebSocket من الطرف الأول لتطبيقات Laravel، يوفر اتصالاً في الوقت الفعلي سريعاً للغاية. تم بناؤه فوق ReactPHP ويوفر تكاملاً سلساً مع Laravel Broadcasting.

مقدمة إلى Laravel Reverb

لماذا نستخدم Reverb؟
  • الأداء: مبني بـ PHP باستخدام ReactPHP، محسّن للإنتاجية العالية
  • من الطرف الأول: حزمة Laravel رسمية مع تكامل أصلي
  • فعّال من حيث التكلفة: بديل ذاتي الاستضافة لـ Pusher/Ably
  • إعداد بسيط: يعمل خارج الصندوق مع Laravel Broadcasting
  • التوسع الأفقي: يمكن توسيعه عبر خوادم متعددة

تثبيت وتكوين Reverb

// تثبيت Reverb composer require laravel/reverb // تثبيت إعدادات Reverb والتبعيات php artisan reverb:install // هذا سيقوم بـ: // - إضافة تكوين reverb إلى config/reverb.php // - تحديث .env بمتغيرات REVERB_* // - تثبيت Laravel Echo و Pusher JS SDK (لجانب العميل) // تكوين .env BROADCAST_CONNECTION=reverb REVERB_APP_ID=my-app-id REVERB_APP_KEY=my-app-key REVERB_APP_SECRET=my-app-secret REVERB_HOST="localhost" REVERB_PORT=8080 REVERB_SCHEME=http // للإنتاج مع SSL REVERB_SCHEME=https REVERB_HOST="your-domain.com"

بدء خادم Reverb

// التطوير - بدء خادم Reverb php artisan reverb:start // مع إخراج التصحيح php artisan reverb:start --debug // الإنتاج - التشغيل كعملية خلفية php artisan reverb:start & // أو استخدم Supervisor (موصى به للإنتاج) // /etc/supervisor/conf.d/reverb.conf [program:reverb] command=php /path/to/artisan reverb:start autostart=true autorestart=true user=www-data redirect_stderr=true stdout_logfile=/var/log/reverb.log
نصائح الإنتاج:
  • استخدم Supervisor أو systemd للحفاظ على تشغيل Reverb
  • ضع Reverb خلف Nginx مع وكيل WebSocket
  • فعّل SSL/TLS لاتصالات WebSocket الآمنة (wss://)
  • استخدم Redis للتوسع الأفقي عبر الخوادم
  • راقب استخدام الذاكرة وأعد التشغيل بشكل دوري

بث الأحداث مع Reverb

// إنشاء حدث قابل للبث php artisan make:event MessageSent // app/Events/MessageSent.php namespace App\Events; use App\Models\Message; 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 MessageSent implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; public function __construct(public Message $message) { } public function broadcastOn(): array { // قناة عامة return [ new Channel('chat.' . $this->message->room_id), ]; // أو قناة خاصة return [ new PrivateChannel('chat.' . $this->message->room_id), ]; // أو قناة الحضور return [ new PresenceChannel('chat.' . $this->message->room_id), ]; } public function broadcastAs(): string { return 'message.sent'; } public function broadcastWith(): array { return [ 'id' => $this->message->id, 'user' => $this->message->user->only(['id', 'name', 'avatar']), 'content' => $this->message->content, 'created_at' => $this->message->created_at->toISOString(), ]; } } // إرسال الحدث use App\Events\MessageSent; $message = Message::create([ 'user_id' => auth()->id(), 'room_id' => $roomId, 'content' => $request->content, ]); broadcast(new MessageSent($message)); // أو event(new MessageSent($message));

التكامل من جانب العميل مع Laravel Echo

// تثبيت Laravel Echo و Pusher JS npm install --save-dev laravel-echo pusher-js // resources/js/bootstrap.js import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; window.Pusher = Pusher; window.Echo = new Echo({ broadcaster: 'reverb', key: import.meta.env.VITE_REVERB_APP_KEY, wsHost: import.meta.env.VITE_REVERB_HOST, wsPort: import.meta.env.VITE_REVERB_PORT ?? 8080, wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', enabledTransports: ['ws', 'wss'], }); // .env VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" VITE_REVERB_HOST="${REVERB_HOST}" VITE_REVERB_PORT="${REVERB_PORT}" VITE_REVERB_SCHEME="${REVERB_SCHEME}" // الاستماع إلى القنوات العامة Echo.channel('chat.1') .listen('.message.sent', (e) => { console.log('رسالة جديدة:', e); appendMessage(e); }); // الاستماع إلى القنوات الخاصة (يتطلب المصادقة) Echo.private('chat.1') .listen('.message.sent', (e) => { appendMessage(e); }); // مغادرة قناة Echo.leave('chat.1');

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

// Controller class ChatController extends Controller { public function sendMessage(Request $request, $roomId) { $request->validate([ 'content' => 'required|string|max:1000', ]); $message = Message::create([ 'user_id' => auth()->id(), 'room_id' => $roomId, 'content' => $request->content, ]); broadcast(new MessageSent($message))->toOthers(); return response()->json($message); } public function typing(Request $request, $roomId) { broadcast(new UserTyping( auth()->user(), $roomId ))->toOthers(); return response()->json(['status' => 'ok']); } } // حدث UserTyping class UserTyping implements ShouldBroadcast { use Dispatchable, SerializesModels; public function __construct( public User $user, public int $roomId ) {} public function broadcastOn(): array { return [new PrivateChannel('chat.' . $this->roomId)]; } public function broadcastAs(): string { return 'user.typing'; } public function broadcastWith(): array { return [ 'user_id' => $this->user->id, 'name' => $this->user->name, ]; } } // مكون الدردشة من جانب العميل <div id="chat-room" data-room-id="{{ $room->id }}"> <div id="messages"></div> <div id="typing-indicator"></div> <form id="message-form"> <input type="text" id="message-input" placeholder="اكتب رسالة..."> <button type="submit">إرسال</button> </form> </div> <script> const roomId = document.getElementById('chat-room').dataset.roomId; const messagesDiv = document.getElementById('messages'); const typingDiv = document.getElementById('typing-indicator'); const messageForm = document.getElementById('message-form'); const messageInput = document.getElementById('message-input'); let typingTimeout; // الاستماع للرسائل الجديدة Echo.private(`chat.${roomId}`) .listen('.message.sent', (e) => { appendMessage(e); }) .listenForWhisper('typing', (e) => { showTyping(e.name); }); // إرسال رسالة messageForm.addEventListener('submit', async (e) => { e.preventDefault(); const content = messageInput.value.trim(); if (!content) return; await fetch(`/chat/rooms/${roomId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, }, body: JSON.stringify({ content }), }); messageInput.value = ''; }); // مؤشر الكتابة messageInput.addEventListener('input', () => { Echo.private(`chat.${roomId}`) .whisper('typing', { name: '{{ auth()->user()->name }}' }); clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { typingDiv.textContent = ''; }, 1000); }); function appendMessage(message) { const div = document.createElement('div'); div.className = 'message'; div.innerHTML = ` <strong>${message.user.name}:</strong> <span>${message.content}</span> <small>${new Date(message.created_at).toLocaleTimeString()}</small> `; messagesDiv.appendChild(div); messagesDiv.scrollTop = messagesDiv.scrollHeight; } function showTyping(name) { typingDiv.textContent = `${name} يكتب...`; setTimeout(() => { typingDiv.textContent = ''; }, 1000); } </script>

قنوات الحضور - من متصل

// تعريف تفويض قناة الحضور في routes/channels.php use Illuminate\Support\Facades\Broadcast; Broadcast::channel('chat.{roomId}', function ($user, $roomId) { if ($user->canAccessRoom($roomId)) { return [ 'id' => $user->id, 'name' => $user->name, 'avatar' => $user->avatar_url, ]; } }); // من جانب العميل - الاستماع إلى قناة الحضور Echo.join(`chat.${roomId}`) .here((users) => { // يُستدعى عند الانضمام، يستقبل جميع المستخدمين الحاليين console.log('حالياً في الغرفة:', users); updateUserList(users); }) .joining((user) => { // يُستدعى عند انضمام مستخدم جديد console.log(user.name + ' انضم'); addUserToList(user); showNotification(`${user.name} انضم إلى الغرفة`); }) .leaving((user) => { // يُستدعى عند مغادرة مستخدم console.log(user.name + ' غادر'); removeUserFromList(user); showNotification(`${user.name} غادر الغرفة`); }) .listen('.message.sent', (e) => { appendMessage(e); }); // مكون قائمة المستخدمين function updateUserList(users) { const userList = document.getElementById('user-list'); userList.innerHTML = users.map(user => ` <div class="user" data-user-id="${user.id}"> <img src="${user.avatar}" alt="${user.name}"> <span>${user.name}</span> <span class="status online"></span> </div> `).join(''); } function addUserToList(user) { const userList = document.getElementById('user-list'); const userDiv = document.createElement('div'); userDiv.className = 'user'; userDiv.dataset.userId = user.id; userDiv.innerHTML = ` <img src="${user.avatar}" alt="${user.name}"> <span>${user.name}</span> <span class="status online"></span> `; userList.appendChild(userDiv); } function removeUserFromList(user) { const userDiv = document.querySelector(`[data-user-id="${user.id}"]`); if (userDiv) { userDiv.remove(); } }
اعتبارات الأداء:
  • استخدم toOthers() لمنع إعادة صدى الأحداث إلى المرسل
  • نفذ التحكم في معدل الأحداث عالية التردد (مؤشرات الكتابة)
  • استخدم whisper للاتصال من عميل إلى عميل بدون رحلة الخادم
  • قم بتحسين حجم البيانات - أرسل البيانات الضرورية فقط
  • فكر في استخدام Redis لتوسيع أفضل مع خوادم Reverb متعددة

أحداث العميل إلى العميل (Whisper)

// أحداث Whisper لا تصل إلى الخادم، فقط للعملاء الآخرين Echo.private('chat.1') .whisper('typing', { name: 'John Doe' }); // الاستماع إلى whispers Echo.private('chat.1') .listenForWhisper('typing', (e) => { console.log(e.name + ' يكتب...'); }); // مثال واقعي: تتبع المؤشر في محرر تعاوني const editor = document.getElementById('editor'); let throttleTimeout; editor.addEventListener('mousemove', (e) => { if (throttleTimeout) return; throttleTimeout = setTimeout(() => { Echo.private('document.1') .whisper('cursor-move', { user: '{{ auth()->user()->name }}', x: e.clientX, y: e.clientY, }); throttleTimeout = null; }, 50); // التحكم بحد أقصى 20 تحديثاً في الثانية }); Echo.private('document.1') .listenForWhisper('cursor-move', (e) => { updateRemoteCursor(e.user, e.x, e.y); });
تمرين 1: ابن نظام إشعارات في الوقت الفعلي:
  1. ثبت وكوّن Laravel Reverb
  2. أنشئ حدث NotificationSent الذي يبث إلى قنوات خاصة خاصة بالمستخدم
  3. أعد تفويض القناة للقنوات الخاصة للمستخدمين
  4. أنشئ مكون جرس الإشعارات الذي يعرض الأعداد في الوقت الفعلي
  5. اعرض إشعارات toast عند وصول إشعارات جديدة
  6. أضف ميزة "تحديد كمقروء" التي تحدث في الوقت الفعلي
تمرين 2: أنشئ محرر مستندات تعاوني:
  1. أعد قناة حضور تعرض جميع المحررين النشطين
  2. ابث تغييرات المحتوى في الوقت الفعلي لجميع المحررين
  3. استخدم أحداث whisper لتتبع موضع المؤشر
  4. نفذ مؤشرات الكتابة التي تعرض من يحرر أي قسم
  5. أضف حل التعارضات للتعديلات المتزامنة
  6. اعرض صور المستخدمين مع مواضع المؤشر
تمرين 3: ابن لوحة معلومات حية مع مقاييس في الوقت الفعلي:
  1. أنشئ أحداثاً لمقاييس مختلفة (المبيعات، المستخدمون المتصلون، إحصائيات الخادم)
  2. ابث المقاييس كل 5 ثوانٍ باستخدام مهمة مجدولة
  3. ابن لوحة معلومات مع رسوم بيانية تتحدث في الوقت الفعلي
  4. أضف إشعارات صوتية للأحداث المهمة
  5. نفذ منطق إعادة الاتصال إذا انقطع WebSocket
  6. اعرض مؤشر حالة الاتصال