الوقت الفعلي مع WebSockets و Reverb
الوقت الفعلي مع WebSockets و Reverb
تحوّل ميزات الوقت الفعلي تجارب المستخدمين من خلال تقديم تحديثات فورية دون تحديث الصفحة. Laravel Reverb، خادم WebSocket الرسمي لـ Laravel، يجعل بناء التطبيقات في الوقت الفعلي بسيطاً بشكل لا يصدق. في هذا الدرس، سنستكشف بث الأحداث وإدارة القنوات وتنفيذ Laravel Echo على الواجهة الأمامية وبناء ميزات واقعية مثل الإشعارات وأنظمة الدردشة.
إعداد Laravel Reverb
Laravel Reverb هو خادم WebSocket من الطرف الأول يتكامل بسلاسة مع نظام البث في Laravel. يوفر بديلاً قابلاً للتوسع وعالي الأداء لخدمات الطرف الثالث مثل Pusher.
# تثبيت Laravel Reverb
composer require laravel/reverb
# نشر التكوين
php artisan reverb:install
# هذا ينشئ:
# - config/reverb.php (تكوين Reverb)
# - يحدّث .env بمتغيرات REVERB_*
# - يحدّث config/broadcasting.php
# تكوين .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
# للإنتاج
REVERB_HOST=reverb.example.com
REVERB_PORT=443
REVERB_SCHEME=https
# بدء خادم Reverb
php artisan reverb:start
# بدء مع وضع التصحيح
php artisan reverb:start --debug
# التشغيل في الخلفية (الإنتاج)
nohup php artisan reverb:start > /dev/null 2>&1 &
<?php
// config/broadcasting.php
'connections' => [
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST', 'localhost'),
'port' => env('REVERB_PORT', 8080),
'scheme' => env('REVERB_SCHEME', 'http'),
'useTLS' => env('REVERB_SCHEME', 'http') === 'https',
],
],
],
// تمكين قائمة الانتظار للبث (موصى به)
'queue' => [
'connection' => env('QUEUE_CONNECTION', 'redis'),
'queue' => 'broadcasts',
],
بث الأحداث
يسمح بث الأحداث في Laravel بدفع الأحداث من جانب الخادم إلى تطبيقات JavaScript من جانب العميل في الوقت الفعلي.
<?php
// إنشاء حدث بث
php artisan make:event MessageSent
// app/Events/MessageSent.php
namespace App\Events;
use App\Models\Message;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
public function __construct(
public Message $message,
public User $sender
) {}
// تحديد القنوات للبث عليها
public function broadcastOn(): array
{
return [
new PrivateChannel('chat.' . $this->message->chat_id),
];
}
// تخصيص اسم حدث البث (الافتراضي: MessageSent)
public function broadcastAs(): string
{
return 'message.sent';
}
// تخصيص بيانات البث
public function broadcastWith(): array
{
return [
'id' => $this->message->id,
'content' => $this->message->content,
'sender' => [
'id' => $this->sender->id,
'name' => $this->sender->name,
'avatar' => $this->sender->avatar_url,
],
'timestamp' => $this->message->created_at->toIso8601String(),
];
}
// قائمة انتظار البث لأداء أفضل
public function broadcastQueue(): string
{
return 'broadcasts';
}
}
// إطلاق الحدث
use App\Events\MessageSent;
public function sendMessage(Request $request)
{
$message = Message::create([
'chat_id' => $request->chat_id,
'user_id' => auth()->id(),
'content' => $request->content,
]);
// البث إلى جميع المشتركين
broadcast(new MessageSent($message, auth()->user()));
return response()->json($message, 201);
}
// بث مشروط
class OrderShipped implements ShouldBroadcast
{
// بث فقط إذا كانت قيمة الطلب عالية
public function broadcastWhen(): bool
{
return $this->order->value > 1000;
}
}
// البث إلى مستخدمين محددين
public function broadcastOn(): array
{
return [
new PrivateChannel('user.' . $this->order->user_id),
new PrivateChannel('admin-notifications'),
];
}
أنواع القنوات: عامة وخاصة وحضور
يدعم Laravel ثلاثة أنواع من القنوات، كل منها بمتطلبات تفويض مختلفة وحالات استخدام.
<?php
// routes/channels.php - تحديد تفويض القناة
// قناة عامة - لا يلزم تفويض
// تُستخدم لـ: الإعلانات العامة، تحديثات الحالة
Broadcast::channel('announcements', function () {
return true; // يمكن للجميع الاستماع
});
// قناة خاصة - يلزم التفويض
// تُستخدم لـ: بيانات خاصة بالمستخدم، محادثات خاصة
Broadcast::channel('chat.{chatId}', function ($user, $chatId) {
// التحقق من أن المستخدم عضو في هذه الدردشة
return $user->chats()->where('id', $chatId)->exists();
});
Broadcast::channel('user.{userId}', function ($user, $userId) {
// يمكن للمستخدمين الاستماع فقط إلى قناتهم الخاصة
return (int) $user->id === (int) $userId;
});
// قناة الحضور - مثل الخاصة، لكن تتبع من متصل
// تُستخدم لـ: غرف الدردشة، التحرير التعاوني، مؤشرات عبر الإنترنت
Broadcast::channel('chat-room.{roomId}', function ($user, $roomId) {
if ($user->canAccessRoom($roomId)) {
return [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar_url,
'status' => 'online',
];
}
});
// تفويض معقد مع ربط النموذج
Broadcast::channel('project.{project}', function ($user, Project $project) {
return $user->id === $project->user_id || $project->team->users->contains($user);
});
// حدث باستخدام أنواع قنوات مختلفة
class UserStatusChanged implements ShouldBroadcast
{
public function broadcastOn(): array
{
return [
// عام: عدد المستخدمين العالمي
new Channel('user-stats'),
// خاص: إخطار أصدقاء المستخدم
...collect($this->user->friends)->map(fn($friend) =>
new PrivateChannel("user.{$friend->id}")
),
// حضور: تحديث جميع الغرف التي يتواجد فيها المستخدم
...collect($this->user->activeRooms)->map(fn($room) =>
new PresenceChannel("chat-room.{$room->id}")
),
];
}
}
Laravel Echo - التكامل الأمامي
Laravel Echo هي مكتبة JavaScript تجعل الاشتراك في القنوات والاستماع للأحداث المبثوثة بواسطة Laravel سلساً على الواجهة الأمامية.
# تثبيت 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 ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
// .env لـ Vite
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
// الاستماع إلى القنوات العامة
Echo.channel('announcements')
.listen('AnnouncementMade', (e) => {
console.log('إعلان جديد:', e.title, e.message);
showNotification(e.title, e.message);
});
// الاستماع إلى القنوات الخاصة (يتطلب مصادقة)
Echo.private(`user.${userId}`)
.listen('.message.sent', (e) => {
console.log('رسالة جديدة:', e);
appendMessage(e);
})
.listen('NotificationCreated', (e) => {
incrementNotificationBadge();
showToast(e.notification);
});
// الانضمام إلى قنوات الحضور (انظر من متصل)
Echo.join(`chat-room.${roomId}`)
.here((users) => {
// يُستدعى عند الانضمام - قائمة المستخدمين الموجودين بالفعل في القناة
console.log('المستخدمون المتصلون حالياً:', users);
updateOnlineUsers(users);
})
.joining((user) => {
// يُستدعى عندما ينضم شخص ما
console.log(user.name + ' انضم');
addUserToOnlineList(user);
})
.leaving((user) => {
// يُستدعى عندما يغادر شخص ما
console.log(user.name + ' غادر');
removeUserFromOnlineList(user);
})
.listen('.message.sent', (e) => {
appendMessage(e);
})
.error((error) => {
console.error('خطأ في القناة:', error);
});
// مغادرة قناة عند إلغاء تحميل المكون
const channel = Echo.private(`chat.${chatId}`);
// لاحقاً...
Echo.leave(`chat.${chatId}`);
// أحداث الهمس (من عميل إلى عميل دون خادم)
// مفيد لمؤشرات الكتابة
Echo.private(`chat.${chatId}`)
.whisper('typing', {
user: currentUser.name
});
Echo.private(`chat.${chatId}`)
.listenForWhisper('typing', (e) => {
showTypingIndicator(e.user);
});
// مثال React/Vue
useEffect(() => {
Echo.private(`user.${user.id}`)
.listen('.message.sent', handleNewMessage)
.notification((notification) => {
toast.success(notification.message);
});
return () => {
Echo.leave(`user.${user.id}`);
};
}, [user.id]);
إشعارات الوقت الفعلي
يتكامل نظام الإشعارات في Laravel بسلاسة مع البث لتقديم إشعارات فورية داخل التطبيق.
<?php
// إنشاء إشعار
php artisan make:notification NewCommentNotification
// app/Notifications/NewCommentNotification.php
namespace App\Notifications;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;
class NewCommentNotification extends Notification implements ShouldBroadcast
{
public function __construct(
public $comment,
public $post
) {}
public function via($notifiable): array
{
return ['database', 'broadcast'];
}
public function toDatabase($notifiable): array
{
return [
'comment_id' => $this->comment->id,
'post_id' => $this->post->id,
'commenter' => $this->comment->user->name,
'message' => $this->comment->user->name . ' علّق على منشورك',
];
}
public function toBroadcast($notifiable): BroadcastMessage
{
return new BroadcastMessage([
'id' => $this->id,
'comment_id' => $this->comment->id,
'post_id' => $this->post->id,
'commenter' => [
'name' => $this->comment->user->name,
'avatar' => $this->comment->user->avatar_url,
],
'message' => $this->comment->user->name . ' علّق على منشورك',
'created_at' => now()->toIso8601String(),
]);
}
public function broadcastOn(): array
{
return [
new PrivateChannel('user.' . $this->post->user_id),
];
}
}
// إرسال الإشعار
$post->user->notify(new NewCommentNotification($comment, $post));
// الواجهة الأمامية - الاستماع للإشعارات
Echo.private(`user.${userId}`)
.notification((notification) => {
console.log('إشعار جديد:', notification);
// تحديث واجهة المستخدم
addNotificationToList(notification);
incrementBadge();
playSound();
// عرض toast
toast.info(notification.message, {
action: {
label: 'عرض',
onClick: () => window.location.href = `/posts/${notification.post_id}`
}
});
});
// جلب الإشعارات غير المقروءة
async function fetchNotifications() {
const response = await fetch('/api/notifications');
const notifications = await response.json();
renderNotifications(notifications);
}
// وضع علامة كمقروء
async function markAsRead(notificationId) {
await fetch(`/api/notifications/${notificationId}/read`, {
method: 'POST'
});
}
// نقطة نهاية API للإشعارات
public function index()
{
return auth()->user()->notifications()->latest()->paginate(20);
}
public function markAsRead($id)
{
auth()->user()->notifications()->findOrFail($id)->markAsRead();
return response()->json(['message' => 'وُضعت علامة كمقروء']);
}
بناء نظام دردشة في الوقت الفعلي
يجمع تطبيق الدردشة الكامل بين القنوات الخاصة وتتبع الحضور وأحداث الهمس لتجربة سلسة في الوقت الفعلي.
<?php
// app/Http/Controllers/ChatController.php
class ChatController extends Controller
{
public function sendMessage(Request $request, $chatId)
{
$validated = $request->validate([
'content' => 'required|string|max:1000',
]);
$message = Message::create([
'chat_id' => $chatId,
'user_id' => auth()->id(),
'content' => $validated['content'],
]);
$message->load('user');
broadcast(new MessageSent($message))->toOthers();
return response()->json($message, 201);
}
public function getMessages($chatId)
{
$messages = Message::where('chat_id', $chatId)
->with('user')
->latest()
->paginate(50);
return response()->json($messages);
}
}
// الواجهة الأمامية - مكون دردشة كامل
class ChatRoom {
constructor(chatId, userId) {
this.chatId = chatId;
this.userId = userId;
this.typingTimeout = null;
this.initializeEcho();
this.loadMessages();
}
initializeEcho() {
// الانضمام إلى قناة الحضور
this.channel = Echo.join(`chat.${this.chatId}`)
.here((users) => {
this.renderOnlineUsers(users);
})
.joining((user) => {
this.addOnlineUser(user);
this.showSystemMessage(`${user.name} انضم`);
})
.leaving((user) => {
this.removeOnlineUser(user);
this.showSystemMessage(`${user.name} غادر`);
})
.listen('.message.sent', (e) => {
this.appendMessage(e);
this.scrollToBottom();
this.playNotificationSound();
})
.listenForWhisper('typing', (e) => {
this.showTypingIndicator(e.user);
this.hideTypingIndicatorAfterDelay();
});
}
async loadMessages() {
const response = await fetch(`/api/chats/${this.chatId}/messages`);
const data = await response.json();
this.renderMessages(data.data);
}
async sendMessage(content) {
const response = await fetch(`/api/chats/${this.chatId}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ content })
});
const message = await response.json();
this.appendMessage(message);
this.scrollToBottom();
}
onTyping(userName) {
clearTimeout(this.typingTimeout);
this.channel.whisper('typing', {
user: userName
});
this.typingTimeout = setTimeout(() => {
this.channel.whisper('stopped-typing', {
user: userName
});
}, 2000);
}
destroy() {
Echo.leave(`chat.${this.chatId}`);
}
}
// الاستخدام
const chat = new ChatRoom(chatId, userId);
// إرسال رسالة
document.querySelector('#send-btn').addEventListener('click', () => {
const input = document.querySelector('#message-input');
chat.sendMessage(input.value);
input.value = '';
});
// مؤشر الكتابة
document.querySelector('#message-input').addEventListener('input', () => {
chat.onTyping(currentUser.name);
});
// التنظيف عند إلغاء تحميل المكون
window.addEventListener('beforeunload', () => {
chat.destroy();
});
تمرين 1: نظام إشعارات الوقت الفعلي
ابنِ نظام إشعارات كامل في الوقت الفعلي:
- أنشئ إشعاراً للمتابعين الجدد (FollowerNotification)
- ابثّ إلى القناة الخاصة للمستخدم المتابَع
- خزّن الإشعارات في قاعدة البيانات وابثّها في نفس الوقت
- نفذ مستمعاً للواجهة الأمامية يحدّث عدد الشارة ويعرض toast
- أضف نقاط نهاية API لجلب الإشعارات ووضع علامة كمقروء
- اختبر مع مستخدمين متعددين وتحقق من التسليم في الوقت الفعلي
تمرين 2: تحديثات لوحة المعلومات المباشرة
أنشئ لوحة معلومات إدارية في الوقت الفعلي:
- ابثّ أحداث OrderPlaced إلى قناة عامة "order-stats"
- قم بتضمين البيانات: عدد الطلبات، إجمالي الإيرادات، الطلبات الأخيرة
- استخدم Echo للاستماع وتحديث مخططات لوحة المعلومات في الوقت الفعلي
- أضف أحداث بث متعددة: UserRegistered، PaymentReceived
- نفذ التحديث التلقائي للمقاييس الحرجة كل 30 ثانية
- اختبر تحديثات لوحة المعلومات عند وضع الطلبات
تمرين 3: محرر نصوص تعاوني
ابنِ ميزة تحرير تعاونية أساسية:
- أنشئ قناة حضور لتحرير المستندات ("document.{id}")
- اعرض من يشاهد/يحرر المستند حالياً
- استخدم أحداث الهمس لمزامنة مواضع المؤشر بين المستخدمين
- ابثّ تغييرات النص (مع تأخير) إلى جميع المشاركين
- أضف مؤشرات "المستخدم X يحرر..."
- اختبر مع نوافذ متصفح متعددة للتحقق من التعاون
الخلاصة
في هذا الدرس، أتقنت الاتصال في الوقت الفعلي في Laravel باستخدام Reverb و WebSockets. تعلمت كيفية إعداد Laravel Reverb وبث الأحداث إلى قنوات عامة/خاصة/حضور وتكامل Laravel Echo على الواجهة الأمامية وتنفيذ إشعارات في الوقت الفعلي وبناء ميزات كاملة مثل أنظمة الدردشة مع مؤشرات الكتابة والحضور عبر الإنترنت. هذه المهارات تمكّنك من إنشاء تطبيقات جذابة وتفاعلية تقدم تحديثات فورية للمستخدمين.