Broadcasting в Laravel позволяет транслировать серверные события на клиентскую сторону через WebSocket-соединения. Это основа для создания real-time функциональности: чатов, уведомлений, live-обновлений и collaborative-инструментов.
Конфигурация
Конфигурация broadcasting находится в config/broadcasting.php.
// config/broadcasting.php
return [
'default' => env('BROADCAST_CONNECTION', 'null'),
'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'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
],
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'host' => env('PUSHER_HOST'),
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'null' => [
'driver' => 'null',
],
'log' => [
'driver' => 'log',
],
],
];
Установка Laravel Reverb
Laravel Reverb -- это first-party WebSocket-сервер, включённый в Laravel 11.
# Install Reverb
php artisan install:broadcasting
# This will:
# 1. Install Laravel Reverb
# 2. Install Laravel Echo
# 3. Create channels.php routes file
# 4. Update .env with Reverb credentials
Создание событий для вещания
Событие должно реализовать интерфейс ShouldBroadcast или ShouldBroadcastNow.
<?php
declare(strict_types=1);
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\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
final class MessageSent implements ShouldBroadcast
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
public function __construct(
public readonly User $user,
public readonly Message $message,
) {}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('chat.' . $this->message->chat_id),
];
}
/**
* The event's broadcast name.
*/
public function broadcastAs(): string
{
return 'message.sent';
}
/**
* Get the data to broadcast.
*
* @return array<string, mixed>
*/
public function broadcastWith(): array
{
return [
'id' => $this->message->id,
'text' => $this->message->text,
'user' => [
'id' => $this->user->id,
'name' => $this->user->name,
],
'created_at' => $this->message->created_at->toISOString(),
];
}
/**
* Determine if this event should broadcast.
*/
public function broadcastWhen(): bool
{
return ! $this->message->is_draft;
}
}
ShouldBroadcast vs ShouldBroadcastNow
// ShouldBroadcast - broadcasting is queued (recommended)
final class OrderStatusChanged implements ShouldBroadcast
{
// Broadcast event is placed on the queue
// and processed by a queue worker
/**
* The queue connection to use for broadcasting.
*/
public string $connection = 'redis';
/**
* The queue name for the broadcast job.
*/
public string $queue = 'broadcasts';
}
// ShouldBroadcastNow - broadcasting is immediate (synchronous)
final class UrgentAlert implements ShouldBroadcastNow
{
// Broadcast event is sent immediately
// without going through the queue
}
Типы каналов
Public Channel (Публичный)
Любой пользователь может подписаться. Не требует авторизации.
use Illuminate\Broadcasting\Channel;
public function broadcastOn(): array
{
return [
new Channel('orders'),
];
}
Private Channel (Приватный)
Требует авторизации. Только авторизованные пользователи могут подписаться.
use Illuminate\Broadcasting\PrivateChannel;
public function broadcastOn(): array
{
return [
new PrivateChannel('orders.' . $this->order->user_id),
];
}
Presence Channel (Канал присутствия)
Расширение приватного канала с информацией о подключённых пользователях. Идеален для функций "кто сейчас онлайн".
use Illuminate\Broadcasting\PresenceChannel;
public function broadcastOn(): array
{
return [
new PresenceChannel('chat.' . $this->chatId),
];
}
Авторизация каналов
Правила авторизации определяются в файле routes/channels.php.
<?php
// routes/channels.php
use App\Models\Chat;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
// Private channel authorization
Broadcast::channel('orders.{userId}', function (User $user, int $userId) {
return $user->id === $userId;
});
// Returning data for private channel
Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) {
$order = Order::find($orderId);
return $order && $user->id === $order->user_id;
});
// Presence channel authorization
// Return data that will be shared with other subscribers
Broadcast::channel('chat.{chatId}', function (User $user, int $chatId) {
$chat = Chat::find($chatId);
if ($chat && $chat->users()->where('user_id', $user->id)->exists()) {
// Return user data for the presence channel
return [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar_url,
];
}
return false;
});
// Using channel classes for complex authorization
Broadcast::channel('project.{projectId}', \App\Broadcasting\ProjectChannel::class);
Channel-классы для авторизации
<?php
declare(strict_types=1);
namespace App\Broadcasting;
use App\Models\Project;
use App\Models\User;
final class ProjectChannel
{
/**
* Authenticate the user's access to the channel.
*
* @return array<string, mixed>|bool
*/
public function join(User $user, Project $project): array|bool
{
if (! $project->team->hasUser($user)) {
return false;
}
// For presence channels, return user data
return [
'id' => $user->id,
'name' => $user->name,
'role' => $project->team->roleFor($user),
];
}
}
Вещание событий
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Events\MessageSent;
use App\Models\Chat;
use App\Models\Message;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
final class MessageController extends Controller
{
public function store(Request $request, Chat $chat): JsonResponse
{
$message = $chat->messages()->create([
'user_id' => $request->user()->id,
'text' => $request->input('text'),
]);
// Method 1: Dispatch the event
event(new MessageSent($request->user(), $message));
// Method 2: Using broadcast() helper
broadcast(new MessageSent($request->user(), $message));
// Method 3: Broadcast to others (exclude sender)
broadcast(new MessageSent($request->user(), $message))->toOthers();
return response()->json($message, 201);
}
}
Метод toOthers()
Метод toOthers() исключает текущего пользователя из трансляции. Это полезно для интерфейсов, где отправитель сразу видит свои действия (optimistic UI).
// The current user will NOT receive this broadcast
broadcast(new MessageSent($user, $message))->toOthers();
Для работы toOthers() на клиенте нужно передать socket ID:
// JavaScript (Laravel Echo)
axios.defaults.headers.common['X-Socket-Id'] = Echo.socketId();
Laravel Echo (клиентская сторона)
Laravel Echo -- JavaScript-библиотека для работы с WebSocket-каналами.
// 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'],
});
Подписка на публичный канал
Echo.channel('orders')
.listen('OrderShipped', (event) => {
console.log('Order shipped:', event.order);
});
Подписка на приватный канал
Echo.private('orders.' + userId)
.listen('OrderStatusChanged', (event) => {
console.log('Order status:', event.status);
})
// Custom broadcast name (with broadcastAs)
.listen('.order.status.changed', (event) => {
console.log('Order status:', event.status);
});
Подписка на presence-канал
Echo.join('chat.' + chatId)
// When you successfully join
.here((users) => {
console.log('Users in chat:', users);
})
// When a new user joins
.joining((user) => {
console.log('User joined:', user.name);
})
// When a user leaves
.leaving((user) => {
console.log('User left:', user.name);
})
// Listen for events
.listen('MessageSent', (event) => {
console.log('New message:', event);
})
// Error handling
.error((error) => {
console.error('Channel error:', error);
});
Whisper (Client Events)
Whisper позволяет отправлять события между клиентами без участия сервера.
// Send typing indicator (client-to-client, no server)
Echo.private('chat.' + chatId)
.whisper('typing', {
user: userName,
});
// Listen for typing
Echo.private('chat.' + chatId)
.listenForWhisper('typing', (event) => {
console.log(event.user + ' is typing...');
});
Модельное вещание (Model Broadcasting)
Laravel позволяет автоматически транслировать события моделей Eloquent.
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Database\Eloquent\BroadcastsEvents;
use Illuminate\Database\Eloquent\Model;
final class Post extends Model
{
use BroadcastsEvents;
protected $fillable = ['title', 'content', 'user_id'];
/**
* Get the channels that model events should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(string $event): array
{
return match ($event) {
'created' => [
new PrivateChannel('users.' . $this->user_id),
],
default => [
new PrivateChannel('posts.' . $this->id),
],
};
}
/**
* Customize the broadcast event name.
*/
public function broadcastAs(string $event): string
{
return "post.{$event}";
}
/**
* Get the data to broadcast for the model event.
*
* @return array<string, mixed>
*/
public function broadcastWith(string $event): array
{
return match ($event) {
'created', 'updated' => [
'id' => $this->id,
'title' => $this->title,
'updated_at' => $this->updated_at->toISOString(),
],
default => ['id' => $this->id],
};
}
}
Подписка на модельные события через Echo:
Echo.private('posts.' + postId)
.listen('.post.updated', (event) => {
console.log('Post updated:', event);
})
.listen('.post.deleted', (event) => {
console.log('Post deleted:', event.id);
});
Notification Broadcasting
Уведомления можно транслировать через broadcasting.
<?php
declare(strict_types=1);
namespace App\Notifications;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;
final class InvoicePaid extends Notification implements ShouldBroadcast
{
public function __construct(
public readonly float $amount,
) {}
/**
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['broadcast', 'database'];
}
public function toBroadcast(object $notifiable): BroadcastMessage
{
return new BroadcastMessage([
'invoice_id' => $this->id,
'amount' => $this->amount,
'message' => "Invoice paid: {$this->amount}",
]);
}
/**
* The channels the notification should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('user.' . $this->notifiable->id),
];
}
public function broadcastType(): string
{
return 'invoice.paid';
}
}
Подписка на уведомления через Echo:
Echo.private('App.Models.User.' + userId)
.notification((notification) => {
console.log('Notification:', notification.type);
console.log('Data:', notification);
});
Тестирование Broadcasting
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Events\MessageSent;
use App\Models\Chat;
use App\Models\Message;
use App\Models\User;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
final class BroadcastingTest extends TestCase
{
public function test_message_event_is_broadcast(): void
{
Event::fake([MessageSent::class]);
$user = User::factory()->create();
$chat = Chat::factory()->create();
$message = Message::factory()->create([
'chat_id' => $chat->id,
'user_id' => $user->id,
]);
event(new MessageSent($user, $message));
Event::assertDispatched(MessageSent::class, function ($event) use ($chat) {
$channels = $event->broadcastOn();
return $channels[0]->name === 'private-chat.' . $chat->id;
});
}
public function test_broadcast_event_contains_correct_data(): void
{
$user = User::factory()->create();
$message = Message::factory()->create(['user_id' => $user->id]);
$event = new MessageSent($user, $message);
$data = $event->broadcastWith();
$this->assertArrayHasKey('id', $data);
$this->assertArrayHasKey('text', $data);
$this->assertArrayHasKey('user', $data);
$this->assertEquals($user->id, $data['user']['id']);
}
}