Advanced Laravel Broadcasting: Ứng dụng Real-Time

· 6 min read

Laravel Broadcasting cho phép bạn broadcast dữ liệu đến các connected clients một cách real-time. Kết hợp với Reverb (WebSocket server của Laravel), bạn có thể xây dựng ứng dụng chat, live notifications, các công cụ cộng tác, và nhiều hơn nữa.

Setup

Cài đặt Reverb:

composer require laravel/reverb
php artisan reverb:install

Cấu hình trong .env:

REVERB_APP_ID=your-app-id
REVERB_APP_KEY=your-app-key
REVERB_APP_SECRET=your-app-secret
REVERB_HOST=localhost
REVERB_PORT=8080

Khởi chạy Reverb:

php artisan reverb:start

Basic Broadcasting Event

Tạo một broadcastable event:

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithBroadcasting;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

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

    public function __construct(
        public Message $message,
    ) {}

    public function broadcastOn(): array
    {
        return [
            new Channel('chat.' . $this->message->conversation_id),
        ];
    }

    public function broadcastAs(): string
    {
        return 'message.sent';
    }

    public function broadcastWith(): array
    {
        return [
            'id' => $this->message->id,
            'text' => $this->message->text,
            'user' => $this->message->user->only(['id', 'name']),
        ];
    }
}

Listen on Frontend

// resources/js/chat.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,
    wssPort: import.meta.env.VITE_REVERB_PORT,
    forceTLS: import.meta.env.VITE_REVERB_SCHEME === 'https',
    enabledTransports: ['ws', 'wss'],
});

// Listen for messages
window.Echo.channel(`chat.${conversationId}`)
    .listen('message.sent', (data) => {
        console.log('New message:', data);
        addMessageToUI(data);
    });

Private Channels

Hạn chế truy cập cho những người dùng cụ thể:

// routes/channels.php
use App\Models\User;
use App\Models\Conversation;

Broadcast::channel('conversation.{id}', function (User $user, int $id) {
    return $user->conversations()->where('id', $id)->exists();
});

Broadcasting đến private channels:

class MessageSent implements ShouldBroadcast
{
    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('conversation.' . $this->message->conversation_id),
        ];
    }
}

Listen on frontend:

window.Echo.private(`conversation.${conversationId}`)
    .listen('message.sent', (data) => {
        addMessageToUI(data);
    });

Presence Channels

Theo dõi người dùng nào đang online và đang xem:

// routes/channels.php
Broadcast::channel('conversation.{id}:presence', function (User $user, int $id) {
    return $user->conversations()->where('id', $id)->exists()
        ? ['id' => $user->id, 'name' => $user->name]
        : false;
});

Broadcasting đến presence channel:

class UserTyping implements ShouldBroadcast
{
    public function broadcastOn(): array
    {
        return [
            new PresenceChannel('conversation.' . $this->conversationId . ':presence'),
        ];
    }

    public function broadcastWith(): array
    {
        return [
            'user_id' => $this->user->id,
            'user_name' => $this->user->name,
        ];
    }
}

Listen on frontend:

window.Echo.join(`conversation.${conversationId}:presence`)
    .here((users) => {
        console.log('Currently online:', users);
    })
    .joining((user) => {
        console.log(user.name + ' joined');
    })
    .leaving((user) => {
        console.log(user.name + ' left');
    })
    .listen('UserTyping', (data) => {
        showTypingIndicator(data.user_name);
    });

Real-Time Notifications

class OrderShipped implements ShouldBroadcast
{
    public function __construct(
        public Order $order,
    ) {}

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

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

    public function broadcastWith(): array
    {
        return [
            'order_id' => $this->order->id,
            'tracking_number' => $this->order->tracking_number,
        ];
    }
}

Frontend:

window.Echo.private(`user.${userId}`)
    .notification((notification) => {
        console.log('Notification:', notification);
    })
    .listen('order.shipped', (data) => {
        showNotification(`Order #${data.order_id} has shipped!`);
    });

Debouncing Broadcasts

Ngăn chặn quá nhiều events:

class UserSearching implements ShouldBroadcast
{
    public function __construct(
        public string $query,
        public int $userId,
    ) {}

    public function broadcastOn(): array
    {
        return [new Channel('search')];
    }
}

// Controller
public function search(Request $request)
{
    event(new UserSearching($request->query, auth()->id()));

    return SearchService::find($request->query);
}

Frontend với debounce:

import { debounce } from 'lodash-es';

const search = debounce((query) => {
    window.Echo.channel('search')
        .listen('UserSearching', (data) => {
            updateSearchResults(data);
        });
}, 300);

document.getElementById('search').addEventListener('input', (e) => {
    search(e.target.value);
});

Conditional Broadcasting

Chỉ broadcast dưới các điều kiện nhất định:

class PostPublished implements ShouldBroadcast
{
    public function __construct(public Post $post) {}

    public function broadcastOn(): array
    {
        return $this->post->is_public
            ? [new Channel('posts')]
            : [];
    }
}

Hoặc kiểm tra on the fly:

public function broadcastWhen(): bool
{
    return $this->post->is_published && !$this->post->is_scheduled;
}

Broadcasting Model Changes

class Post extends Model
{
    protected $dispatchesEvents = [
        'created' => PostCreated::class,
        'updated' => PostUpdated::class,
        'deleted' => PostDeleted::class,
    ];
}

class PostUpdated implements ShouldBroadcast
{
    public function __construct(public Post $post) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('posts.' . $this->post->user_id)];
    }

    public function broadcastWith(): array
    {
        return $this->post->only(['id', 'title', 'content', 'updated_at']);
    }
}

Testing Broadcasting

use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Event;

public function test_message_is_broadcast()
{
    Event::fake();

    $message = Message::factory()->create();

    event(new MessageSent($message));

    Event::assertDispatched(MessageSent::class);
}

Performance Optimization

Queue Broadcasting

Ngăn chặn blocking requests trong broadcast:

class MessageSent implements ShouldBroadcast, ShouldQueue
{
    use Dispatchable, InteractsWithBroadcasting;
    
    public $queue = 'broadcasts';
}

Compress Data

Chỉ broadcast những gì cần thiết:

public function broadcastWith(): array
{
    return [
        'id' => $this->message->id,
        'text' => $this->message->text,
        'user_id' => $this->message->user->id,
        // Không include heavy relationships
    ];
}

Batch Broadcasting

public function broadcastMultiple(array $userIds)
{
    foreach ($userIds as $userId) {
        event(new Notification($userId, $data));
    }
    
    // Hoặc sử dụng batch
    Bus::batch(array_map(
        fn($id) => new BroadcastNotification($id, $data),
        $userIds
    ))->dispatch();
}

Best Practices

  1. Validate channel access - Luôn kiểm tra authorization
  2. Compress data - Không broadcast huge payloads
  3. Use queues - Broadcast một cách asynchronous
  4. Limit listeners - Không broadcast cho mọi người
  5. Handle reconnections - Client-side retry logic
  6. Monitor connections - Theo dõi active users
  7. Test thoroughly - Test broadcasting logic

Common Patterns

Live Comments

class CommentPosted implements ShouldBroadcast
{
    public function broadcastOn()
    {
        return new PrivateChannel('post.' . $this->comment->post_id);
    }
}

Live Notifications

class Notification implements ShouldBroadcast, ShouldQueue
{
    public function broadcastOn()
    {
        return new PrivateChannel('user.' . $this->user_id);
    }
}

Collaboration

class DocumentUpdated implements ShouldBroadcast
{
    public function broadcastOn()
    {
        return new PrivateChannel('document.' . $this->document_id);
    }
}

Tóm tắt

Laravel Broadcasting với Reverb cho phép các tính năng real-time:

  • Public channels - Broadcast cho mọi người
  • Private channels - Hạn chế cho những người dùng cụ thể
  • Presence channels - Theo dõi những người dùng online
  • Queue broadcasting - Non-blocking broadcasts
  • Model events - Auto-broadcast on changes
  • Client-side listening - JavaScript integration

Xây dựng ứng dụng responsive, real-time một cách dễ dàng.

Bình luận