<?php
use App\Models\Conversation;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('chat.{conversationId}', function (User $user, int $conversationId) {
$conversation = Conversation::find($conversationId);
if (! $conversation) {
return false;
}
return $conversation->participates($user);
});
<?php
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcast, ShouldQueue
{
use Dispatchable, SerializesModels;
public function __construct(public Message $message)
{
}
public function broadcastOn(): PrivateChannel
{
return new PrivateChannel('chat.' . $this->message->conversation_id);
}
public function broadcastAs(): string
{
return 'message.sent';
}
public function broadcastWith(): array
{
return [
'id' => $this->message->id,
'body' => $this->message->body,
'author' => $this->message->user->name,
'author_id' => $this->message->user_id,
'sent_at' => $this->message->created_at->toIso8601String(),
];
}
}
<?php
namespace App\Http\Controllers;
use App\Events\MessageSent;
use App\Models\Conversation;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SendMessageController extends Controller
{
public function store(Request $request, Conversation $conversation): JsonResponse
{
$this->authorize('participate', $conversation);
$validated = $request->validate([
'body' => ['required', 'string', 'max:2000'],
]);
$message = $conversation->messages()->create([
'user_id' => $request->user()->id,
'body' => $validated['body'],
]);
broadcast(new MessageSent($message->load('user')))->toOthers();
return response()->json([
'id' => $message->id,
'body' => $message->body,
'author' => $request->user()->name,
'sent_at' => $message->created_at->toIso8601String(),
], 201);
}
}
<div
x-data="{ messages: @js($messages) }"
x-init="$nextTick(() => $refs.scroller.scrollTop = $refs.scroller.scrollHeight)"
x-on:message-sent.window="messages.push($event.detail)"
>
<div
x-ref="scroller"
class="chat-scroller"
@echo-private("chat.{{ $conversation->id }}", "message.sent", (e) => {
messages.push(e);
$nextTick(() => $refs.scroller.scrollTop = $refs.scroller.scrollHeight);
})
>
{{-- Initial server-rendered history --}}
@foreach ($messages as $message)
<x-chat.message :message="$message" />
@endforeach
{{-- Live messages appended over WebSocket --}}
<template x-for="message in messages.slice({{ count($messages) }})" :key="message.id">
<div class="chat-message">
<span class="chat-author" x-text="message.author"></span>
<p class="chat-body" x-text="message.body"></p>
</div>
</template>
</div>
</div>
This snippet shows the full round-trip of a real-time chat message in Laravel: persisting it, broadcasting it over a private channel, and rendering it client-side into a Blade-defined DOM node. The core idea is that broadcasting decouples the HTTP request that creates a message from the delivery of that message to every other connected participant. The sender's request returns immediately after the write, while a queued event fans the payload out to WebSocket subscribers.
In MessageSent event, the class implements ShouldBroadcast, which is what tells Laravel to push this event onto a broadcast driver (Pusher, Reverb, or Ably) rather than only dispatching it in-process. broadcastOn() returns a PrivateChannel scoped to the conversation id, so only authorized members receive it. broadcastAs() renames the wire event to a stable message.sent string instead of the fully-qualified class name, which keeps the JavaScript listener decoupled from PHP namespaces. broadcastWith() shapes a lean payload — deliberately not the whole model — to avoid leaking columns and to keep frames small. Implementing ShouldQueue alongside ShouldBroadcast means the actual push happens on a worker, so a slow WebSocket provider never blocks the web request.
SendMessageController handles the create path. It validates input, associates the message with the authenticated user via the relationship, and then calls broadcast(new MessageSent($message))->toOthers(). The toOthers() modifier is important: it excludes the current socket connection from delivery so the sender does not receive a duplicate of a message it already rendered optimistically. The controller returns the saved model as JSON for the sender's own UI.
channels.php defines the authorization callback for chat.{conversationId}. Private and presence channels require this gate; returning a truthy value authorizes the subscription, and here membership is checked through participates(). Without this, PrivateChannel subscriptions are rejected.
Finally, message-list.blade.php pairs a server-rendered <x-chat.message> component template with an Alpine listener bound through Echo. The echo-private directive subscribes to the channel, and incoming message.sent events are appended to a reactive array, reusing the same markup contract as the initial server render. The trade-off is added infrastructure (a broadcast server plus queue workers) in exchange for push updates without polling.
Related snips
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
{
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
Laravel mix/Vite for asset compilation
class Comment < ApplicationRecord
belongs_to :article
belongs_to :author, class_name: "User"
validates :body, presence: true, length: { maximum: 2_000 }
Live comments with model broadcasts + turbo_stream_from
<%# private stream: turbo signs the serialized record name %>
<%= turbo_stream_from current_user %>
<section class="notifications">
<h1>Notifications</h1>
Turbo Streams + authorization: signed per-user stream name
// Basic event listener
const button = document.getElementById('myButton');
button.addEventListener('click', function(event) {
console.log('Button clicked!');
console.log('Event type:', event.type);
Event handling and event delegation patterns in JavaScript
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
Laravel database migrations for schema management
Share this code
Here's the card — post it anywhere.