Laravel's event broadcasting system provides real-time capabilities through WebSockets. While Pusher is the default driver, you can implement custom broadcasters for services like PubNub, Ably, or your own WebSocket server. This guide covers implementing custom broadcasting from a senior developer's perspective.
Why Custom Broadcasting
Custom broadcasters enable:
- Alternative Services: Use PubNub, Ably, or self-hosted solutions
- Cost Control: Avoid Pusher pricing for high-volume apps
- Specific Features: Leverage unique provider capabilities
- Compliance: Meet data residency requirements
- Integration: Connect with existing infrastructure
Laravel Broadcasting Basics
Configure Broadcasting
config/broadcasting.php:
<?php
return [
'default' => env('BROADCAST_DRIVER', 'null'),
'connections' => [
'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'),
'useTLS' => true,
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
// Custom broadcaster
'pubnub' => [
'driver' => 'pubnub',
'publish_key' => env('PUBNUB_PUBLISH_KEY'),
'subscribe_key' => env('PUBNUB_SUBSCRIBE_KEY'),
'secret_key' => env('PUBNUB_SECRET_KEY'),
'server_uuid' => env('PUBNUB_SERVER_UUID'),
],
],
];
Environment Configuration
.env:
BROADCAST_DRIVER=pubnub
PUBNUB_PUBLISH_KEY=pub-c-xxxxx
PUBNUB_SUBSCRIBE_KEY=sub-c-xxxxx
PUBNUB_SECRET_KEY=sec-c-xxxxx
PUBNUB_SERVER_UUID=server-unique-id
Creating a Custom Broadcaster
Install Provider SDK
composer require pubnub/pubnub
Create Broadcaster Class
app/Broadcasting/PubNubBroadcaster.php:
<?php
namespace App\Broadcasting;
use Illuminate\Broadcasting\Broadcasters\Broadcaster;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use PubNub\PNConfiguration;
use PubNub\PubNub;
use PubNub\Exceptions\PubNubException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class PubNubBroadcaster extends Broadcaster
{
protected PubNub $pubnub;
protected array $config;
public function __construct(array $config)
{
$this->config = $config;
$pnConfig = new PNConfiguration();
$pnConfig->setPublishKey($config['publish_key']);
$pnConfig->setSubscribeKey($config['subscribe_key']);
$pnConfig->setSecretKey($config['secret_key']);
$pnConfig->setUuid($config['server_uuid']);
$this->pubnub = new PubNub($pnConfig);
}
/**
* Authenticate the incoming request for a given channel.
*/
public function auth($request)
{
$channelName = $request->channel_name;
// Handle private and presence channels
if (Str::startsWith($channelName, ['private-', 'presence-'])) {
return $this->verifyUserCanAccessChannel(
$request,
$channelName
);
}
// Public channels don't require auth
return true;
}
/**
* Return the valid authentication response.
*/
public function validAuthenticationResponse($request, $result)
{
if (Str::startsWith($request->channel_name, 'presence-')) {
return [
'channel' => $request->channel_name,
'auth_key' => $this->generateAuthKey($request->channel_name),
'data' => [
'user_id' => $result['user_id'] ?? null,
'user_info' => $result['user_info'] ?? [],
],
];
}
return [
'channel' => $request->channel_name,
'auth_key' => $this->generateAuthKey($request->channel_name),
];
}
/**
* Broadcast the given event.
*/
public function broadcast(array $channels, $event, array $payload = [])
{
$socket = Arr::pull($payload, 'socket');
// Add event name to payload
$payload['event'] = class_basename($event);
try {
foreach ($this->formatChannels($channels) as $channel) {
$this->pubnub->publish()
->channel($channel)
->message($payload)
->sync();
}
} catch (PubNubException $e) {
throw new BroadcastException(
sprintf('PubNub error: %s', $e->getMessage())
);
}
}
/**
* Get the PubNub SDK instance.
*/
public function getPubNub(): PubNub
{
return $this->pubnub;
}
/**
* Generate an authentication key for the channel.
*/
protected function generateAuthKey(string $channel): string
{
return hash_hmac('sha256', $channel, $this->config['secret_key']);
}
/**
* Verify the user can access the channel.
*/
protected function verifyUserCanAccessChannel($request, string $channelName)
{
$channelName = Str::startsWith($channelName, 'private-')
? Str::replaceFirst('private-', '', $channelName)
: Str::replaceFirst('presence-', '', $channelName);
return parent::verifyUserCanAccessChannel(
$request,
$channelName
);
}
}
Register the Broadcaster
app/Providers/BroadcastServiceProvider.php:
<?php
namespace App\Providers;
use App\Broadcasting\PubNubBroadcaster;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
public function boot()
{
// Register custom broadcaster
Broadcast::extend('pubnub', function ($app, $config) {
return new PubNubBroadcaster($config);
});
// Only register routes for drivers that need them
if (config('broadcasting.default') !== 'pubnub') {
Broadcast::routes();
}
require base_path('routes/channels.php');
}
}
Broadcasting Events
Create Broadcastable Event
<?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 Message $message;
public function __construct(Message $message)
{
$this->message = $message;
}
/**
* Get the channels the event should broadcast on.
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('chat.' . $this->message->room_id),
];
}
/**
* The event's broadcast name.
*/
public function broadcastAs(): string
{
return 'message.sent';
}
/**
* Get the data to broadcast.
*/
public function broadcastWith(): array
{
return [
'id' => $this->message->id,
'content' => $this->message->content,
'user' => [
'id' => $this->message->user->id,
'name' => $this->message->user->name,
],
'created_at' => $this->message->created_at->toISOString(),
];
}
}
Define Channel Authorization
routes/channels.php:
<?php
use Illuminate\Support\Facades\Broadcast;
// Private channel - simple authorization
Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
return $user->rooms()->where('id', $roomId)->exists();
});
// Presence channel - return user data
Broadcast::channel('room.{roomId}', function ($user, $roomId) {
if ($user->rooms()->where('id', $roomId)->exists()) {
return [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar_url,
];
}
return false;
});
// Public channel (no auth needed)
Broadcast::channel('updates', function () {
return true;
});
Dispatch Events
// In controller or service
use App\Events\MessageSent;
public function sendMessage(Request $request, Room $room)
{
$message = $room->messages()->create([
'user_id' => auth()->id(),
'content' => $request->content,
]);
// Broadcast immediately
broadcast(new MessageSent($message));
// Or broadcast to others (excluding sender)
broadcast(new MessageSent($message))->toOthers();
return response()->json($message);
}
Frontend Integration
PubNub JavaScript Client
import PubNub from 'pubnub';
const pubnub = new PubNub({
publishKey: process.env.PUBNUB_PUBLISH_KEY,
subscribeKey: process.env.PUBNUB_SUBSCRIBE_KEY,
uuid: 'user-' + userId,
});
// Subscribe to channel
pubnub.subscribe({
channels: ['private-chat.1'],
});
// Listen for messages
pubnub.addListener({
message: (event) => {
const { channel, message } = event;
console.log('Received:', message);
// Handle based on event type
if (message.event === 'message.sent') {
addMessageToChat(message);
}
},
presence: (event) => {
console.log('Presence:', event.action, event.uuid);
},
});
// Publish message (usually done via the Laravel API instead)
pubnub.publish({
channel: 'private-chat.1',
message: { content: 'Hello!' },
});
Vue.js Component Example
<template>
<div class="chat">
<div class="messages">
<div v-for="msg in messages" :key="msg.id" class="message">
<strong>{{ msg.user.name }}:</strong>
{{ msg.content }}
</div>
</div>
<form @submit.prevent="sendMessage">
<input v-model="newMessage" placeholder="Type a message..." />
<button type="submit">Send</button>
</form>
</div>
</template>
<script>
import PubNub from 'pubnub';
export default {
props: ['roomId', 'user'],
data() {
return {
messages: [],
newMessage: '',
pubnub: null,
};
},
created() {
this.initPubNub();
this.fetchMessages();
},
beforeDestroy() {
this.pubnub.unsubscribe({ channels: [this.channelName] });
},
computed: {
channelName() {
return `private-chat.${this.roomId}`;
},
},
methods: {
initPubNub() {
this.pubnub = new PubNub({
subscribeKey: process.env.VUE_APP_PUBNUB_SUBSCRIBE_KEY,
uuid: `user-${this.user.id}`,
});
this.pubnub.addListener({
message: (event) => {
if (event.message.event === 'message.sent') {
this.messages.push(event.message);
}
},
});
this.pubnub.subscribe({ channels: [this.channelName] });
},
async fetchMessages() {
const response = await fetch(`/api/rooms/${this.roomId}/messages`);
this.messages = await response.json();
},
async sendMessage() {
if (!this.newMessage.trim()) return;
await fetch(`/api/rooms/${this.roomId}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({ content: this.newMessage }),
});
this.newMessage = '';
},
},
};
</script>
Using Laravel WebSockets
For self-hosted WebSocket servers:
composer require beyondcode/laravel-websockets
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider"
php artisan migrate
Configure config/websockets.php and start:
php artisan websockets:serve
Testing Broadcasting
<?php
namespace Tests\Feature;
use App\Events\MessageSent;
use App\Models\Message;
use App\Models\User;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class BroadcastingTest extends TestCase
{
public function test_message_sent_event_is_broadcast()
{
Event::fake();
$user = User::factory()->create();
$message = Message::factory()->create(['user_id' => $user->id]);
broadcast(new MessageSent($message));
Event::assertDispatched(MessageSent::class, function ($event) use ($message) {
return $event->message->id === $message->id;
});
}
public function test_broadcast_data_format()
{
$message = Message::factory()->create();
$event = new MessageSent($message);
$data = $event->broadcastWith();
$this->assertArrayHasKey('id', $data);
$this->assertArrayHasKey('content', $data);
$this->assertArrayHasKey('user', $data);
}
}
Key Takeaways
- Extend Broadcaster: Implement the Broadcaster contract
- Register in service provider: Use Broadcast::extend()
- Handle auth properly: Implement channel authorization
- Use broadcastWith(): Control what data is sent
- Test with Event::fake(): Verify events are dispatched
- Consider queuing: Use ShouldBroadcastNow for immediate delivery
Custom broadcasters give you the flexibility to use any real-time service while maintaining Laravel's elegant event-driven architecture.