Sobre nós Guias Projetos Contactos
Админка
please wait

O sistema de event broadcasting do Laravel disponibiliza capacidades em tempo real através de WebSockets. Embora o Pusher seja o driver predefinido, pode implementar broadcasters personalizados para serviços como PubNub, Ably, ou o seu próprio servidor WebSocket. Este guia aborda a implementação de broadcasting personalizado na perspetiva de um developer sénior.

Porquê Broadcasting Personalizado

Os broadcasters personalizados permitem:

  1. Serviços Alternativos: Utilizar PubNub, Ably, ou soluções self-hosted
  2. Controlo de Custos: Evitar os preços do Pusher em apps de elevado volume
  3. Funcionalidades Específicas: Tirar partido de capacidades únicas do fornecedor
  4. Conformidade: Cumprir requisitos de residência de dados
  5. Integração: Ligar-se à infraestrutura existente

Noções Básicas de Broadcasting no Laravel

Configurar 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',
],
// Broadcaster personalizado
'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'),
],
],
];

Configuração de Ambiente

.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

Criar um Broadcaster Personalizado

Instalar o SDK do Fornecedor

composer require pubnub/pubnub

Criar a Classe Broadcaster

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);
}
/**
* Autenticar o pedido recebido para um determinado channel.
*/
public function auth($request)
{
$channelName = $request->channel_name;
// Tratar channels private e presence
if (Str::startsWith($channelName, ['private-', 'presence-'])) {
return $this->verifyUserCanAccessChannel(
$request,
$channelName
);
}
// Os channels public não requerem autenticação
return true;
}
/**
* Devolver a resposta de autenticação válida.
*/
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),
];
}
/**
* Fazer broadcast do event fornecido.
*/
public function broadcast(array $channels, $event, array $payload = [])
{
$socket = Arr::pull($payload, 'socket');
// Adicionar o nome do event ao 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())
);
}
}
/**
* Obter a instância do SDK do PubNub.
*/
public function getPubNub(): PubNub
{
return $this->pubnub;
}
/**
* Gerar a chave de autenticação para o channel.
*/
protected function generateAuthKey(string $channel): string
{
return hash_hmac('sha256', $channel, $this->config['secret_key']);
}
/**
* Verificar se o utilizador pode aceder ao 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
);
}
}

Registar o 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()
{
// Registar broadcaster personalizado
Broadcast::extend('pubnub', function ($app, $config) {
return new PubNubBroadcaster($config);
});
// Apenas registar routes para drivers que precisem delas
if (config('broadcasting.default') !== 'pubnub') {
Broadcast::routes();
}
require base_path('routes/channels.php');
}
}

Fazer Broadcasting de Events

Criar um Event com Broadcasting

<?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;
}
/**
* Obter os channels em que o event deve fazer broadcast.
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('chat.' . $this->message->room_id),
];
}
/**
* O nome de broadcast do event.
*/
public function broadcastAs(): string
{
return 'message.sent';
}
/**
* Obter os dados a transmitir por 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(),
];
}
}

Definir Autorização do Channel

routes/channels.php:

<?php
use Illuminate\Support\Facades\Broadcast;
// Channel private - autorização simples
Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
return $user->rooms()->where('id', $roomId)->exists();
});
// Channel presence - devolver dados do utilizador
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;
});
// Channel public (não é necessária autenticação)
Broadcast::channel('updates', function () {
return true;
});

Despachar Events

// No controller ou service
use App\Events\MessageSent;
public function sendMessage(Request $request, Room $room)
{
$message = $room->messages()->create([
'user_id' => auth()->id(),
'content' => $request->content,
]);
// Fazer broadcast imediatamente
broadcast(new MessageSent($message));
// Ou fazer broadcast para outros (excluindo o remetente)
broadcast(new MessageSent($message))->toOthers();
return response()->json($message);
}

Integração no Frontend

Cliente JavaScript do PubNub

import PubNub from 'pubnub';
const pubnub = new PubNub({
publishKey: process.env.PUBNUB_PUBLISH_KEY,
subscribeKey: process.env.PUBNUB_SUBSCRIBE_KEY,
uuid: 'user-' + userId,
});
// Subscrever o channel
pubnub.subscribe({
channels: ['private-chat.1'],
});
// Ouvir mensagens
pubnub.addListener({
message: (event) => {
const { channel, message } = event;
console.log('Received:', message);
// Tratar com base no tipo de event
if (message.event === 'message.sent') {
addMessageToChat(message);
}
},
presence: (event) => {
console.log('Presence:', event.action, event.uuid);
},
});
// Publicar mensagem (normalmente feito através da API do Laravel em vez disso)
pubnub.publish({
channel: 'private-chat.1',
message: { content: 'Hello!' },
});

Exemplo de Componente Vue.js

<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>

Utilizar Laravel WebSockets

Para servidores WebSocket self-hosted:

composer require beyondcode/laravel-websockets
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider"
php artisan migrate

Configure config/websockets.php e inicie:

php artisan websockets:serve

Testar 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);
}
}

Principais Conclusões

  1. Estender Broadcaster: Implementar o contrato Broadcaster
  2. Registar no service provider: Utilizar Broadcast::extend()
  3. Tratar a autenticação corretamente: Implementar a autorização do channel
  4. Usar broadcastWith(): Controlar que dados são enviados
  5. Testar com Event::fake(): Verificar que os events são despachados
  6. Considerar enfileiramento: Utilizar ShouldBroadcastNow para entrega imediata

Os broadcasters personalizados dão-lhe flexibilidade para utilizar qualquer serviço em tempo real, mantendo a elegante arquitetura orientada a events do Laravel.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive