يحتاج تطبيق الدردشة، ولوحة متابعة الطلبات، والإشعارات الفورية، ومؤشرات لوحة التحكم الحية إلى قناة تدفع التغيير إلى المتصفح فور حدوثه. هنا يأتي Laravel Reverb: خادم WebSocket رسمي عالي الأداء، يندمج مباشرة مع منظومة بث الأحداث في Laravel ويتيح تشغيل البنية اللحظية داخل بيئتك بدل الاعتماد الإلزامي على مزوّد خارجي.

يعتمد هذا الدليل على توثيق Laravel 13.x الرسمي المتاح وقت الكتابة. راجع متطلبات إصدار مشروعك قبل تنفيذ الأوامر في بيئة قائمة.

ما Laravel Reverb؟

Laravel Reverb هو خادم WebSocket رسمي مفتوح المصدر لتطبيقات Laravel. يحافظ على اتصالات طويلة العمر بين العملاء والخادم، ويستقبل الرسائل ويوزّع الأحداث على المشتركين بسرعة. وهو متوافق مع بروتوكول Pusher، لذلك يعمل بسلاسة مع طبقة Broadcasting في Laravel ومع مكتبة Laravel Echo في الواجهة الأمامية، ويمكن لأي عميل يدعم بروتوكول Pusher الاتصال به.

في HTTP التقليدي يرسل المتصفح طلبًا ثم ينتظر الاستجابة؛ ولا يعرف بحدوث تغيير جديد إلا بطلب آخر أو عبر polling دوري. أما WebSocket فيبدأ بترقية اتصال HTTP إلى قناة ثنائية الاتجاه تبقى مفتوحة. بعد ذلك يستطيع الخادم دفع الحدث فورًا دون انتظار طلب جديد من العميل، كما يستطيع العميل إرسال رسائل وفق الحدود التي يسمح بها التطبيق.

يصلح Reverb لحالات مثل غرف الدردشة، تتبع حالة الشحن، الإشعارات داخل التطبيق، المزادات الحية، مؤشرات التداول، لوحات العمليات، التعاون بين المستخدمين، وعرض الأشخاص الموجودين داخل صفحة أو غرفة. لكنه ليس بديلًا عن قاعدة البيانات أو الطوابير؛ بل طبقة نقل لحظية تعمل معهما.

متى لا تحتاج Reverb؟

  • إذا كانت البيانات تتغير كل بضع دقائق ويقبل المستخدم تأخيرًا بسيطًا، فقد يكون polling بسيط (مثل wire:poll في Livewire) أقل كلفة تشغيلية.
  • إذا كان التدفق من الخادم إلى العميل فقط وبحجم صغير، فقد تكفي Server-Sent Events أو مزوّد مُدار.
  • إذا لم يكن لدى فريقك قدرة على تشغيل عملية طويلة العمر ومراقبتها، فخدمة مُدارة (Laravel Cloud أو Pusher أو Ably) أنسب في البداية، ويمكن الانتقال لاحقًا لأن الكود في Laravel لا يتغير تقريبًا.

كيف تعمل Reverb داخل Laravel؟

تتكون الدورة المعتادة من خمسة أجزاء مترابطة:

  1. حدث Laravel: يحدث تغيير في الخادم، مثل تحديث حالة طلب، ثم يُطلق التطبيق Event يطبّق ShouldBroadcast.
  2. الطابور: يحوّل Laravel بث الحدث افتراضيًا إلى مهمة queued job حتى لا يطيل زمن استجابة طلب HTTP.
  3. Broadcasting driver: يرسل اتصال reverb حمولة الحدث إلى خادم Reverb عبر HTTP API (المسار /apps) باستخدام بيانات التطبيق الموقّعة بالسر.
  4. خادم Reverb: يعرف الاتصالات والقنوات النشطة، ثم يمرّر الرسالة إلى العملاء المشتركين في القناة المقصودة عبر WebSocket (المسار /app).
  5. Laravel Echo: يشترك المتصفح في القناة ويستمع إلى اسم الحدث، ثم يحدّث الواجهة دون إعادة تحميل الصفحة.
[Controller] → Event::dispatch → [Queue] → [Worker] → HTTP POST /apps/... → [Reverb]
                                                                         ↓ WebSocket /app/...
[Browser + Echo] ← ─────────────────────────────────────────────────────── ┘
       ↑ للقنوات الخاصة: POST /broadcasting/auth → callback في routes/channels.php

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

المكوّنات الأساسية

  • laravel/reverb: خادم WebSocket وتكوين تطبيقاته واتصالاته.
  • Laravel Broadcasting: تعريف الأحداث والقنوات وأسماء الأحداث وحمولاتها وشروط بثها.
  • Laravel Echo: واجهة JavaScript للاشتراك والاستماع وإدارة الاتصال، مع حزم جاهزة لـReact وVue وSvelte.
  • pusher-js: عميل البروتوكول الذي يستخدمه إعداد Echo الخاص بـReverb.
  • عامل Queue: ينفّذ مهام البث غير المتزامنة.
  • Redis: مطلوب عند التوسع الأفقي لمزامنة الرسائل بين عقد Reverb، وليس شرطًا لتشغيل عقدة واحدة.

الفرق بين Reverb وEcho وPusher وAbly وMercure

يأتي Laravel بأربعة drivers بث رسمية: Reverb وPusher Channels وAbly وMercure، إضافة إلى driver log للتطوير وnull لتعطيل البث في الاختبارات.

الأداةالدورمكان التشغيلمتى تختارها؟
Laravel Reverbخادم WebSocket وBroadcasting driver رسميخوادمك، أو مُدار عبر Laravel Cloudعند الرغبة في تكامل Laravel أصلي وتحكم بالبنية والتكلفة
Laravel Echoعميل JavaScript للاشتراك والاستماعالمتصفح أو الواجهة الأماميةتستخدمه مع أي driver مما سبق؛ ليس خادمًا منافسًا لها
Pusher Channelsخدمة بث WebSocket مُدارةبنية Pusherعندما تفضّل عدم إدارة خوادم WebSocket
Ablyمنصة Real-Time مُدارةبنية Ablyعندما تحتاج خدمة مُدارة وخصائص منصتها
MercureHub يعتمد Server-Sent EventsHub تديره أنت أو مُدارعندما يكفيك تدفق أحادي من الخادم إلى العميل أو لديك Hub قائم

الاختيار ليس مقارنة سرعة مجردة. Reverb يمنحك سيطرة تشغيلية أكبر، لكنه ينقل إليك مسؤوليات TLS، وإدارة العملية، والحدود، والمراقبة، والتوسع. الخدمات المُدارة تقلل عبء التشغيل مقابل تكلفة واعتماد على طرف خارجي. وتقدّم Laravel Cloud خيارًا وسطًا: بنية WebSocket مُدارة بالكامل تعمل بعناقيد Reverb.

تثبيت Laravel Reverb وإعداده

المسار السريع الموصى به

البث غير مفعّل افتراضيًا في تطبيقات Laravel الجديدة. فعّله واختر Reverb بالأمر الرسمي:

php artisan install:broadcasting --reverb
npm install
npm run dev

يثبّت الأمر حزم Composer وNPM المطلوبة، ويشغّل reverb:install خلف الكواليس، وينشئ أو يحدّث ملفات الإعداد ومنها config/broadcasting.php وconfig/reverb.php وroutes/channels.php، ويضيف متغيرات البيئة. ويمكن تشغيل php artisan install:broadcasting دون الخيار ثم اختيار Reverb من الأسئلة التفاعلية.

التثبيت اليدوي

composer require laravel/reverb
php artisan reverb:install

بعد التثبيت تأكد من أن BROADCAST_CONNECTION=reverb، ثم راجع بيانات التطبيق. يجب أن تتطابق القيم التي يستخدمها Laravel لإرسال الأحداث مع القيم التي يتحقق منها Reverb:

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

VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"

تنبيه أمني: القيمة REVERB_APP_KEY تصل إلى الواجهة عبر VITE_REVERB_APP_KEY، وهي معرّف عام وليست سرًا. أما REVERB_APP_SECRET فسر خاص بالخادم يوقّع طلبات البث، ولا يجوز أبدًا تضمينه في متغير يبدأ بـVITE_ أو في JavaScript أو في مستودع Git. ولا تخلط بينهما وبين APP_KEY الخاص بـLaravel؛ فهو مفتاح التشفير الرئيسي للتطبيق وسرّي تمامًا.

إعداد Laravel Echo يدويًا

إن لم تستخدم أمر التثبيت، ثبّت الحزم أولًا (يتطلب broadcaster الخاص بـReverb إصدار laravel-echo 1.16.0 أو أحدث):

npm install --save-dev laravel-echo pusher-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 ?? 80,
    wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
    enabledTransports: ['ws', 'wss'],
});

ضع الإعداد عادة في resources/js/echo.js واستورده من ملف JavaScript الرئيسي. تذكّر أن متغيرات VITE_* تُدمج داخل ملفات JavaScript وقت البناء، لذلك يجب إعادة البناء (npm run build) كلما تغيّرت.

تشغيل الخادم والعامل

php artisan reverb:start
php artisan queue:work

يستمع Reverb افتراضيًا على 0.0.0.0:8080. للتطوير تستطيع استخدام منفذ مخصص أو وضع التشخيص:

php artisan reverb:start --host=127.0.0.1 --port=9000
php artisan reverb:start --debug

فرّق بين REVERB_SERVER_HOST وREVERB_SERVER_PORT اللذين يحددان عنوان الاستماع الداخلي للعملية، وبين REVERB_HOST وREVERB_PORT اللذين يحددان الوجهة التي يستخدمها Laravel والعملاء. في الإنتاج قد يستمع Reverb داخليًا على 8080 بينما يظهر للعالم عبر wss://ws.example.com:443.

WSS محليًا مع Herd أو Valet

إذا كان موقعك المحلي يعمل بـHTTPS عبر Laravel Herd أو أمر valet secure، يستطيع Reverb استخدام الشهادة نفسها مباشرة بتمرير اسم الموقع:

php artisan reverb:start --host="0.0.0.0" --port=8080 --hostname="laravel.test"

يصبح الخادم متاحًا عبر wss://laravel.test:8080، ويتجنب ذلك أخطاء Mixed Content أثناء التطوير. ويمكن بدلًا من ذلك تحديد شهادة يدويًا عبر خيارات tls في config/reverb.php.

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

القناة العامة Public Channel

تسمح لأي عميل يعرف اسمها بالاشتراك دون مصادقة. استخدمها فقط لبيانات عامة فعلًا، مثل إعلان منشور عام. يمثلها Channel.

القناة الخاصة Private Channel

تتطلب مصادقة وتفويضًا عبر تطبيق Laravel. يمثلها PrivateChannel، وتُعرّف قاعدة التفويض في routes/channels.php. عند الاشتراك يرسل Echo تلقائيًا طلبًا إلى /broadcasting/auth، فينفّذ Laravel الـcallback المطابق. لا تعتمد على صعوبة تخمين اسم القناة؛ القرار الأمني الحقيقي هو callback التفويض.

تدعم callbacks القنوات ربط النماذج (route model binding) كما في المسارات العادية، ما يجعل القاعدة أوضح:

use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('orders.{order}', function (User $user, Order $order) {
    return $user->id === $order->user_id;
});

لاحظ أن ربط النماذج هنا لا يدعم التقييد التلقائي (scoping) كما في مسارات HTTP، لذلك اجعل الشرط داخل الـcallback صريحًا. وإذا كان المستخدم غير مسجّل الدخول يُرفض التفويض تلقائيًا دون تنفيذ الـcallback. ولاستخدام حارس غير الافتراضي (مثل حارس إداري أو API):

Broadcast::channel('admin.alerts', function ($admin) {
    return $admin->is_active;
}, ['guards' => ['web', 'admin']]);

فئات القنوات Channel Classes

عندما يكبر routes/channels.php، انقل منطق التفويض إلى فئة مستقلة قابلة للاختبار وحقن الاعتماديات:

php artisan make:channel OrderChannel
// routes/channels.php
use App\Broadcasting\OrderChannel;

Broadcast::channel('orders.{order}', OrderChannel::class);

// app/Broadcasting/OrderChannel.php
public function join(User $user, Order $order): array|bool
{
    return $user->can('view', $order);
}

استخدام Policy داخل join() كما في المثال يوحّد قواعد الوصول بين صفحات HTTP والقنوات اللحظية. ولعرض كل قواعد التفويض المسجلة استخدم php artisan channel:list.

قناة الحضور Presence Channel

تبني على القناة الخاصة وتضيف معرفة المشتركين الموجودين. بدل إرجاع true تعيد callback مصفوفة صغيرة من بيانات المستخدم المسموح كشفها لبقية الأعضاء، أو false/null للرفض:

Broadcast::channel('chat.{roomId}', function (User $user, int $roomId) {
    if (! $user->rooms()->whereKey($roomId)->exists()) {
        return false;
    }

    return ['id' => $user->id, 'name' => $user->name];
});

كل ما تعيده هذه المصفوفة يراه جميع أعضاء القناة، فلا تضع فيها البريد الإلكتروني أو رقم الهاتف أو أي حقل داخلي.

مثال عملي: تحديث حالة طلب لحظيًا

سنرسل الحالة الجديدة إلى صاحب الطلب عبر قناة خاصة، مع حمولة محدودة بدل تسلسل نموذج Eloquent كامل.

1. إنشاء الحدث

php artisan make:event OrderStatusUpdated
<?php

namespace App\Events;

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

class OrderStatusUpdated implements ShouldBroadcast, ShouldDispatchAfterCommit, ShouldRescue
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(public Order $order) {}

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

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

    public function broadcastWith(): array
    {
        return [
            'id' => $this->order->id,
            'status' => $this->order->status,
            'updated_at' => $this->order->updated_at?->toISOString(),
        ];
    }
}

ثلاثة قرارات في هذا الحدث تستحق التوضيح:

  • ShouldDispatchAfterCommit: لأن الحدث يعتمد على بيانات قد تتغير داخل transaction. من دونه قد تُنفَّذ مهمة البث قبل تثبيت المعاملة فتقرأ بيانات قديمة. وإذا كان خيار after_commit مفعّلًا في اتصال الطابور، يعالج Laravel ذلك لجميع المهام.
  • ShouldRescue: البث غالبًا ميزة مكمّلة، فإذا تعذّر الوصول إلى الطابور أو حدث خطأ أثناء البث، يُسجَّل الاستثناء في معالج الأخطاء ويكمل الطلب بدل أن يرى المستخدم صفحة خطأ.
  • InteractsWithSockets: مطلوب لاستخدام toOthers() لاحقًا.

2. إطلاق الحدث

$order->update(['status' => 'shipped']);

OrderStatusUpdated::dispatch($order->fresh());

إذا حدّثت واجهة المستخدم محليًا من استجابة HTTP ولا تريد أن تستقبل الجلسة نفسها نسخة مكررة، استخدم:

broadcast(new OrderStatusUpdated($order->fresh()))->toOthers();

تعمل toOthers() بقراءة الترويسة X-Socket-ID من الطلب. يضيفها Echo تلقائيًا إذا كنت تستخدم نسخة Axios العامة؛ أما مع fetch أو عميل HTTP آخر فأضفها يدويًا:

fetch('/orders/42/ship', {
    method: 'POST',
    headers: { 'X-Socket-ID': window.Echo.socketId() },
});

ولبث سريع دون إنشاء فئة حدث كاملة، تتيح واجهة Broadcast الأحداث المجهولة (anonymous events):

Broadcast::private('orders.'.$order->id)
    ->as('order.status.updated')
    ->with(['id' => $order->id, 'status' => $order->status])
    ->send();

3. الاستماع في الواجهة

window.Echo.private(`orders.${orderId}`)
    .listen('.order.status.updated', (event) => {
        document.querySelector('[data-order-status]').textContent = event.status;
    });

النقطة قبل اسم الحدث مطلوبة عند استخدام broadcastAs() باسم مخصص، لأنها تمنع Echo من إضافة البادئة App\Events. وعند مغادرة الصفحة، ألغِ الاشتراك لتجنب الاتصالات أو المستمعين غير الضروريين:

window.Echo.leave(`orders.${orderId}`);

4. لا تثق بأن كل حدث سيصل

WebSocket ليس طابور رسائل مضمون التسليم: إذا انقطع اتصال المستخدم دقيقة، فالأحداث التي بُثّت خلالها لن تُعاد. لذلك عامل البث كإشعار بالتغيير، وأعد جلب الحالة الحالية من API عند إعادة الاتصال أو عند عودة التبويب إلى الواجهة:

window.Echo.connector.pusher.connection.bind('connected', () => {
    refreshOrderStatus(orderId); // طلب HTTP يجلب الحالة الحالية
});

مثال سريع لقناة حضور

window.Echo.join(`chat.${roomId}`)
    .here((users) => renderUsers(users))
    .joining((user) => addUser(user))
    .leaving((user) => removeUser(user))
    .listen('NewMessage', (event) => appendMessage(event.message))
    .error((error) => console.error(error));

تُستدعى here فور الانضمام بقائمة الأعضاء الحاليين، وerror عندما يعيد endpoint التفويض حالة غير 200.

الاستخدام مع React وVue وSvelte

توفّر Echo حزمًا رسمية بخطافات (hooks) جاهزة: @laravel/echo-react و@laravel/echo-vue و@laravel/echo-svelte، وتستخدمها حزم البداية (starter kits) الرسمية. أهم ميزة فيها أنها تغادر القناة تلقائيًا عند إزالة المكوّن، فتختفي مشكلة المستمعين المنسيين.

// إعداد مرة واحدة عند تشغيل التطبيق
import { configureEcho } from '@laravel/echo-react';

configureEcho({ broadcaster: 'reverb' });
import { useEcho, useConnectionStatus } from '@laravel/echo-react';

type OrderEvent = { id: number; status: string; updated_at: string };

function OrderStatus({ orderId }: { orderId: number }) {
    const [status, setStatus] = useState<string>();
    const connection = useConnectionStatus();

    useEcho<OrderEvent>(`orders.${orderId}`, '.order.status.updated', (e) => {
        setStatus(e.status);
    });

    return (
        <div>
            {connection !== 'connected' && <small>جارٍ إعادة الاتصال…</small>}
            <span>{status}</span>
        </div>
    );
}
  • useEcho: للقنوات الخاصة، ويقبل مصفوفة أحداث ونوع TypeScript للحمولة.
  • useEchoPublic وuseEchoPresence: للقنوات العامة وقنوات الحضور.
  • useEchoModel: للاستماع إلى بث نماذج Eloquent.
  • useConnectionStatus: يعيد إحدى الحالات connected أو connecting أو reconnecting أو disconnected أو failed، وهو مفيد لإظهار مؤشر اتصال للمستخدم.

الواجهة نفسها متاحة في Vue وSvelte مع اختلاف الاستيراد فقط.

ميزات إضافية تستحق المعرفة

أحداث العميل Whisper

لمؤشرات مثل "فلان يكتب الآن" لا داعي لإرسال طلب إلى Laravel؛ يرسل العميل حدثًا مباشرة إلى بقية المشتركين في قناة خاصة أو قناة حضور:

window.Echo.private(`chat.${roomId}`)
    .whisper('typing', { name: currentUser.name });

window.Echo.private(`chat.${roomId}`)
    .listenForWhisper('typing', (e) => showTyping(e.name));

بما أن هذه الرسائل لا تمر بالخادم، فلا تستخدمها لأي شيء يحتاج تحققًا أو حفظًا، وطبّق throttle على الإرسال في الواجهة حتى لا تُرسل رسالة مع كل ضغطة مفتاح.

بث الإشعارات Notifications

إذا كان لديك Notification يستخدم قناة broadcast، فاستقبلها عبر قناة المستخدم الخاصة التي يضيف أمر التثبيت قاعدة تفويضها افتراضيًا:

window.Echo.private(`App.Models.User.${userId}`)
    .notification((notification) => showToast(notification.type));

بث تغييرات النماذج تلقائيًا

بإضافة الـtrait BroadcastsEvents إلى نموذج Eloquent وتعريف broadcastOn(string $event)، يبث النموذج تلقائيًا أحداث الإنشاء والتحديث والحذف باسم مثل .PostUpdated. هذه الطريقة سريعة، لكنها تبث النموذج كاملًا افتراضيًا، لذلك عرّف broadcastWith() لتقليص الحمولة قبل استخدامها مع نماذج تحتوي بيانات حساسة.

أحداث Reverb الداخلية

يطلق Reverb أحداث Laravel عادية خلال دورة حياة الاتصال، ويمكنك الاستماع إليها للتسجيل أو الإحصاءات: ChannelCreated وChannelRemoved وConnectionPruned وMessageReceived وMessageSent ضمن المساحة Laravel\Reverb\Events. اجعل المستمعين عليها خفيفين جدًا لأنها تعمل داخل عملية Reverb نفسها.

اختبار البث

اختبر ثلاثة أشياء منفصلة: أن الحدث يُطلق، وأن حمولته والقناة صحيحة، وأن قاعدة التفويض ترفض المستخدم الخطأ.

use App\Events\OrderStatusUpdated;
use Illuminate\Support\Facades\Event;

it('broadcasts the new status to the order owner', function () {
    Event::fake([OrderStatusUpdated::class]);

    $order = Order::factory()->create();

    $this->actingAs($order->user)
        ->post("/orders/{$order->id}/ship")
        ->assertOk();

    Event::assertDispatched(OrderStatusUpdated::class, function ($event) use ($order) {
        return $event->broadcastOn()[0]->name === "private-orders.{$order->id}"
            && $event->broadcastWith()['status'] === 'shipped';
    });
});

في بيئة الاختبار اضبط BROADCAST_CONNECTION=null في phpunit.xml حتى لا تحاول الاختبارات الاتصال بخادم Reverb حقيقي. ولاختبار التفويض، استخرج المنطق إلى Channel Class أو Policy واختبره كوحدة مستقلة.

إعداد Laravel Reverb في الإنتاج

الفصل بين المنفذ الداخلي والعنوان العام

REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080

REVERB_HOST=ws.example.com
REVERB_PORT=443
REVERB_SCHEME=https

إذا كان Reverb يعمل على الخادم نفسه مع Nginx، فاجعل REVERB_SERVER_HOST=127.0.0.1 بدل 0.0.0.0 حتى لا يكون المنفذ الداخلي متاحًا من الشبكة أصلًا.

اجعل Nginx ينهي TLS ويوجّه اتصالات WebSocket إلى Reverb. يجب أن يمرر مساري /app لاتصالات WebSocket و/apps لطلبات API، وأن يحافظ على ترويسات الترقية:

server {
    listen 443 ssl;
    http2 on;
    server_name ws.example.com;

    ssl_certificate     /etc/letsencrypt/live/ws.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ws.example.com/privkey.pem;

    location / {
        proxy_http_version 1.1;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header SERVER_PORT $server_port;
        proxy_set_header REMOTE_ADDR $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_read_timeout 300s;
        proxy_pass http://127.0.0.1:8080;
    }
}

المهلة proxy_read_timeout الافتراضية في Nginx (60 ثانية) قد تغلق الاتصالات الخاملة؛ يرسل بروتوكول Pusher رسائل ping دورية تحافظ عليها عادة، لكن رفع المهلة يمنح هامشًا آمنًا. وإذا كان أمامك load balancer سحابي فاضبط مهلة الخمول فيه أيضًا. ولا تعرض منفذ 8080 للعامة، واضبط firewall وفق ذلك. إذا كنت تستخدم Laravel Forge فإنه يضبط هذا الإعداد تلقائيًا.

إدارة العملية

Reverb عملية طويلة العمر، لذلك استخدم Supervisor أو مدير عمليات مماثل لإعادة تشغيلها عند الفشل أو إقلاع الخادم:

[program:reverb]
command=php /var/www/example.com/artisan reverb:start
directory=/var/www/example.com
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/supervisor/reverb.log
stopasgroup=true
killasgroup=true

وارفع حد الملفات الذي يفتحه Supervisor نفسه في قسم [supervisord] من ملف supervisord.conf، وإلا ورثت عملية Reverb حدًا منخفضًا مهما رفعته في النظام:

[supervisord]
minfds=10000

ولا تنسَ برنامجًا مماثلًا لعامل الطابور (queue:work) أو Horizon؛ فمن دونه لن يُبث أي حدث.

النشر

العملية طويلة العمر لا ترى تغييرات الكود أو الإعداد دون إعادة تشغيل. أضف إلى سكربت النشر:

php artisan config:cache
php artisan reverb:restart
php artisan queue:restart

ينهي reverb:restart الاتصالات بسلاسة ثم يعيد مدير العمليات تشغيل الخادم، فيعيد Echo الاتصال تلقائيًا من جهة العميل.

أمان Laravel Reverb

  • WSS في الإنتاج: استخدم TLS حتى لا تمر الجلسات والرسائل بنص واضح، وأنهِ TLS عبر Nginx غالبًا.
  • حصر Origins: حدّد allowed_origins لكل تطبيق في config/reverb.php. الطلب من Origin غير موجود يُرفض. تجنب * في الإنتاج ما لم تكن الحالة عامة ومقصودة.
  • تفويض كل قناة خاصة: اربط القرار بالمستخدم والكيان المطلوب، وليس بمجرد تسجيل الدخول. ويُفضَّل إعادة استخدام Policies الموجودة.
  • تقليل بيانات الحدث: استخدم broadcastWith() ولا تبث النموذج كاملًا إذا كان يحتوي حقولًا شخصية أو داخلية؛ فكل الخصائص العامة في الحدث تُبث افتراضيًا.
  • حماية الأسرار: احتفظ بـREVERB_APP_SECRET على الخادم فقط، ودوّر القيم عند الاشتباه بتسريبها.
  • عزل منفذ الخدمة: اسمح للـreverse proxy والخدمات المطلوبة فقط بالوصول إلى المنفذ الداخلي.
  • مصادقة مناسبة: تأكد من أن endpoint التفويض /broadcasting/auth يعمل بالحارس المناسب لتطبيق الويب أو الـAPI (مثل Sanctum)، وبسياسة CORS وCSRF صحيحة.
  • الحذر مع Whisper: أحداث العميل لا تمر بالخادم، فلا تعتمد عليها لأي قرار أو بيانات موثوقة.
  • عدم الثقة بالعميل: الرسالة اللحظية لتحسين الواجهة، وليست دليلًا نهائيًا لصلاحية عملية مالية أو إدارية؛ تحقّق على الخادم دائمًا.

يمكن لخادم Reverb واحد خدمة عدة تطبيقات عبر مصفوفة apps في config/reverb.php، ولكل تطبيق بيانات اعتماد وOrigins خاصة. لا تخلط صلاحيات التطبيقات، وافصلها منطقيًا بوضوح.

'apps' => [
    [
        'app_id' => env('REVERB_APP_ID'),
        'key' => env('REVERB_APP_KEY'),
        'secret' => env('REVERB_APP_SECRET'),
        'allowed_origins' => ['app.example.com'],
        // ...
    ],
],

الأداء والتوسع

كل اتصال WebSocket يبقى في الذاكرة ويمثل ملفًا مفتوحًا في أنظمة Unix. لذلك لا يكفي حساب معدل طلبات HTTP؛ راقب الاتصالات المتزامنة، وحجم الرسائل، وعدد الرسائل في الثانية، واستهلاك الذاكرة والملفات والمنافذ.

حد الملفات المفتوحة

ulimit -n

لرفع الحد لمستخدم العملية، عدّل /etc/security/limits.conf:

www-data  soft  nofile  10000
www-data  hard  nofile  10000

Event Loop

يعتمد Reverb داخليًا على ReactPHP event loop. يستخدم افتراضيًا stream_select الذي لا يحتاج امتدادًا إضافيًا، لكنه محدود عادة بنحو 1024 ملفًا مفتوحًا. إذا كنت تستهدف أكثر من ألف اتصال متزامن، تحتاج loop غير مقيد بهذا الحد؛ ويتحول Reverb تلقائيًا إلى ext-uv عندما يكون متاحًا:

pecl install uv

Nginx والمنافذ

اضبط worker_rlimit_nofile وworker_connections في nginx.conf وفق اختبار حمل واقعي:

worker_rlimit_nofile 10000;

events {
    worker_connections 10000;
    multi_accept on;
}

راقب أيضًا نطاق المنافذ المحلية في Linux، لأن كل اتصال يمر عبر الـproxy يستهلك منفذًا:

cat /proc/sys/net/ipv4/ip_local_port_range
# 32768    60999

النطاق الافتراضي أعلاه يعني سقفًا يقارب 28 ألف اتصال لكل خادم. يمكن توسيعه عبر /etc/sysctl.conf، لكن التوسع الأفقي هو الحل الموصى به بعد هذا الحد.

التوسع الأفقي عبر Redis

REVERB_SCALING_ENABLED=true

عند تشغيل عدة عقد Reverb، تستخدم الحزمة إمكانات publish/subscribe في اتصال Redis الافتراضي لنشر الرسالة إلى بقية العقد. ضع العقد خلف load balancer يوزع الاتصالات، ووصلها جميعًا بخادم Redis مركزي مخصص وموثوق. اختبر سلوك موازن الحمل والمهل الزمنية مع الاتصالات طويلة العمر.

قِس قبل أن تقرر

لا يوجد رقم سحري لسعة الخادم؛ فالاتصال الخامل لا يساوي غرفة دردشة عالية النشاط، والحمولات الصغيرة لا تساوي بث نماذج ضخمة. نفّذ load test يحاكي عدد الاتصالات الحقيقي وتواتر الرسائل وحجمها وموجات إعادة الاتصال الجماعية (مثلًا بعد نشر جديد)، ثم ضع هامشًا للأعطال والذروة. أدوات مثل k6 أو Artillery تدعم سيناريوهات WebSocket.

المراقبة والتشخيص عبر Laravel Pulse

يتكامل Reverb رسميًا مع Laravel Pulse لعرض عدد الاتصالات والرسائل. بعد تثبيت Pulse، أضف recorders التالية إلى config/pulse.php:

use Laravel\Reverb\Pulse\Recorders\ReverbConnections;
use Laravel\Reverb\Pulse\Recorders\ReverbMessages;

'recorders' => [
    ReverbConnections::class => ['sample_rate' => 1],
    ReverbMessages::class => ['sample_rate' => 1],
],

ثم أضف البطاقتين إلى لوحة Pulse:

<x-pulse>
    <livewire:reverb.connections cols="full" />
    <livewire:reverb.messages cols="full" />
</x-pulse>

شغّل daemon الآتي على خادم Reverb كي تظهر البيانات بصورة صحيحة، ويفضَّل تحت Supervisor أيضًا:

php artisan pulse:check

في بنية Reverb المتوسعة أفقيًا شغّل pulse:check على خادم واحد فقط.

تشخيص الاتصال خطوة بخطوة

  1. المتصفح: افتح أدوات المطور ← Network ← فلتر WS. يجب أن ترى اتصالًا إلى /app/{key} بحالة 101، ورسائل الاشتراك والأحداث في تبويب Messages.
  2. التفويض: ابحث عن طلب /broadcasting/auth؛ إن أعاد 403 فالمشكلة في القاعدة أو الحارس، وإن أعاد 419 فالمشكلة في CSRF.
  3. الطابور: تأكد أن العامل يعمل وأن php artisan queue:failed لا يحتوي مهام بث فاشلة.
  4. الخادم: شغّل مؤقتًا reverb:start --debug لرؤية تدفق البيانات، ثم أطفئه؛ لا تتركه نشطًا في الإنتاج بسبب الضوضاء والكلفة واحتمال ظهور بيانات حساسة في السجلات.
  5. عزل المشكلة: غيّر مؤقتًا BROADCAST_CONNECTION=log لترى في storage/logs إن كان Laravel يبث الحدث أصلًا.

الأخطاء الشائعة وحلولها

العَرَضالسبب المرجحالفحص أو الحل
الاتصال يعمل لكن الحدث لا يصلعامل queue متوقفشغّل php artisan queue:work وافحص failed jobs
خطأ 403 عند قناة خاصةفشل التفويض أو guard غير صحيحراجع routes/channels.php وchannel:list والجلسة وendpoint التفويض
خطأ 419 عند /broadcasting/authرمز CSRF مفقود أو منتهٍتأكد من وجود meta الـCSRF أو من إعداد Sanctum للـSPA
فشل WSS أو Mixed Contentالصفحة HTTPS والاتصال WS أو شهادة/Proxy خاطئاستخدم https و443 واضبط TLS وترويسات Upgrade
الاتصال يُرفض مباشرةOrigin غير مدرج أو مفتاح خاطئراجع allowed_origins وتطابق REVERB_APP_KEY مع VITE_REVERB_APP_KEY
الواجهة لا تزال تتصل بعنوان قديمأصول Vite لم تُبن بعد تغيير البيئةأعد تشغيل build وانشر الملفات الجديدة
الحدث يصل باسم غير متوقعاستخدام broadcastAs() دون نقطة في Echoاستمع إلى .custom.event
العميل يرى الحدث مرتينتحديث محلي ثم استقبال البث نفسهاستخدم toOthers() وتأكد من إرسال X-Socket-ID
toOthers() لا تؤثرالطلب لا يحمل ترويسة X-Socket-IDاستخدم Axios العام أو أضف Echo.socketId() يدويًا
بيانات قديمة أو ModelNotFoundالبث سبق commit قاعدة البياناتاستخدم ShouldDispatchAfterCommit أو after_commit
المستخدم يفقد تحديثات بعد انقطاع الشبكةWebSocket لا يعيد الأحداث الفائتةأعد جلب الحالة عند حدث connected
يتوقف الاتصال بعد النشرReverb لم يُعد تشغيلهنفّذ php artisan reverb:restart مع مدير عمليات
السعة تتوقف قرب ألف اتصالحد stream_select أو الملفاتثبّت ext-uv واضبط nofile وminfds وNginx

أفضل الممارسات

  1. صمّم أسماء القنوات حول الموارد والصلاحيات مثل orders.{order}، واجعل التفويض قابلًا للاختبار عبر Channel Classes وPolicies.
  2. اعتبر القنوات العامة عامة بالكامل، ولا تضع فيها أي بيانات لا تريد كشفها.
  3. استخدم broadcastWith() لعقد بيانات صغير ومستقر، وأضف version عند الحاجة لتطوير payload.
  4. اترك البث على queue افتراضيًا، وخصّص طابورًا مستقلًا عبر broadcastQueue() أو السمتين #[Connection] و#[Queue] إذا احتجت عزل الحمل. استخدم ShouldBroadcastNow فقط عندما تفهم أثر التنفيذ المتزامن على زمن الطلب.
  5. استخدم broadcastWhen() لمنع أحداث لا يحتاجها العميل بدل بثها ثم تجاهلها.
  6. أضف ShouldRescue للأحداث المكمّلة حتى لا يُفشل تعطّل البث طلب المستخدم.
  7. عامل البث كإشعار بالتغيير لا كمصدر الحقيقة، وأعد المزامنة من API بعد إعادة الاتصال.
  8. افصل خادم Reverb عن web workers منطقيًا، وراقب كل عملية بمدير خدمات.
  9. اختبر إعادة الاتصال وفقد الشبكة وتغيير تبويب المتصفح، وليس المسار المثالي فقط.
  10. نفّذ إعادة تشغيل سلسة عند كل نشر يمس الكود أو الإعداد.
  11. راقب الاتصالات والرسائل والطوابير والذاكرة وRedis وموازن الحمل معًا.
  12. ابدأ بعقدة واحدة، ثم توسع أفقيًا بناءً على قياسات فعلية وخطة تحمل فشل العقدة.

قائمة تحقق قبل الإطلاق

  • ☐ BROADCAST_CONNECTION=reverb وقيم REVERB_* صحيحة، وREVERB_APP_SECRET غير موجود في أي متغير VITE_*.
  • ☐ أصول Vite مبنية بالقيم العامة الصحيحة (REVERB_HOST و443 وhttps).
  • ☐ Nginx ينهي TLS ويمرر /app و/apps مع ترويسات Upgrade، والمنفذ الداخلي مغلق أمام العامة.
  • ☐ allowed_origins محددة بنطاقاتك فقط.
  • ☐ Reverb وعامل الطابور وpulse:check تحت Supervisor، مع minfds مرتفع.
  • ☐ ext-uv مثبت إن كنت تتوقع أكثر من ألف اتصال، وحدود nofile وNginx مرفوعة.
  • ☐ كل قناة خاصة لها اختبار يثبت رفض المستخدم غير المخوّل.
  • ☐ الحمولات محدودة عبر broadcastWith()، وبيانات أعضاء الحضور لا تكشف حقولًا حساسة.
  • ☐ سكربت النشر يشغّل reverb:restart وqueue:restart.
  • ☐ الواجهة تعيد مزامنة الحالة بعد إعادة الاتصال وتعرض مؤشر حالة الاتصال.
  • ☐ اختبار حمل نُفّذ بأرقام قريبة من الذروة المتوقعة.

الأسئلة الشائعة

هل Reverb بديل عن Laravel Echo؟

لا. Reverb خادم WebSocket، بينما Echo مكتبة عميل في الواجهة. غالبًا تستخدمهما معًا.

هل Redis مطلوب دائمًا؟

لا تحتاجه عقدة Reverb واحدة لمجرد تشغيل WebSocket. يصبح Redis جزءًا أساسيًا من إعداد Reverb الرسمي عند تفعيل التوسع الأفقي بين عدة خوادم.

هل أحتاج Queue Worker؟

نعم في المسار المعتاد، لأن الأحداث التي تطبق ShouldBroadcast تُبث عبر queued jobs. يمكن استخدام ShouldBroadcastNow للبث عبر sync queue، لكن ذلك ينقل الكلفة إلى التنفيذ الحالي.

هل يمكن تشغيل عدة تطبيقات على خادم Reverb واحد؟

نعم، تدعم الحزمة تعريف عدة عناصر داخل apps في config/reverb.php، ولكل تطبيق بيانات اعتماد وإعدادات Origins.

هل يجب تشغيل Reverb على المنفذ 443؟

ليس داخليًا. النمط الشائع أن يستمع على 8080 خلف Nginx، بينما يتصل العميل علنًا عبر WSS على 443.

ما الفرق بين Private وPresence؟

كلتاهما تتطلبان التفويض. تضيف Presence معلومات عن الأعضاء الموجودين وأحداث الانضمام والمغادرة، ولذلك يجب تقليل بيانات العضو المعادة.

هل يكفي إخفاء اسم القناة لحمايتها؟

لا. يجب استخدام قناة خاصة أو حضور وتعريف قاعدة Authorization تتحقق من حق المستخدم في المورد المطلوب.

هل يضمن Reverb وصول كل رسالة؟

لا. يسلّم الرسالة للمتصلين لحظة البث فقط. إذا كان الاستلام المضمون مهمًا، احفظ البيانات في قاعدة البيانات واجعل العميل يعيد المزامنة بعد إعادة الاتصال.

هل يعمل Reverb مع تطبيقات الجوال؟

نعم. بما أنه متوافق مع بروتوكول Pusher، يمكن لمكتبات عميل Pusher الرسمية على iOS وAndroid وFlutter الاتصال به بتوجيهها إلى مضيفك ومفتاحك، مع ضبط endpoint التفويض للعمل بالتوكن.

كيف أعرف قدرة الخادم؟

بالاختبار تحت حمل يمثل منتجك، مع مراقبة عدد الملفات والمنافذ والذاكرة والرسائل والطوابير. لا تعتمد على عدد اتصالات نظري منفرد.

الخلاصة

يمنح Laravel Reverb تطبيقات Laravel مسارًا رسميًا لبناء خصائص Real-Time من دون مغادرة نموذج الأحداث والقنوات والتفويض المألوف في الإطار. بداية صحيحة تعني تثبيت Broadcasting وEcho، تعريف أحداث صغيرة وآمنة، تشغيل queue worker وReverb، ثم نقل TLS إلى reverse proxy وإدارة العمليات بصورة دائمة. وعندما يرتفع الحمل، تصبح حدود الملفات وevent loop وRedis والموازن والمراقبة عناصر من تصميم المنتج لا تفاصيل مؤجلة.

قوة Reverb ليست في فتح اتصال WebSocket فقط، بل في دمج النقل اللحظي مع Authorization والطوابير والأحداث وPulse ضمن تجربة Laravel موحدة. وإذا طبّقت القياس والأمان والنشر السلس منذ البداية، تستطيع الانتقال من إشعار بسيط إلى بنية متعددة العقد دون إعادة اختراع طبقة البث.

المراجع الرسمية