<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderStatusUpdated implements ShouldBroadcast
{
use Dispatchable, SerializesModels;
public Order $order;
public function __construct(Order $order)
{
$this->order = $order;
}
public function broadcastOn(): PrivateChannel
{
return new PrivateChannel('order.' . $this->order->id);
}
public function broadcastAs(): string
{
return 'status.updated';
}
public function broadcastWith(): array
{
return [
'id' => $this->order->id,
'status' => $this->order->status,
'updated_at' => $this->order->updated_at->toIso8601String(),
];
}
}
<?php
namespace App\Http\Controllers;
use App\Events\OrderStatusUpdated;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class OrderController extends Controller
{
public function updateStatus(Request $request, Order $order)
{
$this->authorize('update', $order);
$validated = $request->validate([
'status' => 'required|in:placed,preparing,out_for_delivery,delivered,cancelled',
]);
DB::transaction(function () use ($order, $validated) {
$order->update(['status' => $validated['status']]);
$order->statusHistory()->create(['status' => $validated['status']]);
});
broadcast(new OrderStatusUpdated($order->fresh()));
return response()->json([
'id' => $order->id,
'status' => $order->status,
]);
}
}
<?php
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('order.{orderId}', function (User $user, int $orderId) {
$order = Order::find($orderId);
if (! $order) {
return false;
}
return (int) $order->user_id === (int) $user->id;
});
import { useEffect, useState } from 'react';
import Echo from '../lib/echo';
export function useOrderStatus(orderId, initialStatus) {
const [status, setStatus] = useState(initialStatus);
const [updatedAt, setUpdatedAt] = useState(null);
useEffect(() => {
if (!orderId) return;
const channel = Echo.private(`order.${orderId}`);
channel.listen('.status.updated', (payload) => {
setStatus(payload.status);
setUpdatedAt(payload.updated_at);
});
return () => {
Echo.leaveChannel(`private-order.${orderId}`);
};
}, [orderId]);
return { status, updatedAt };
}
This snippet shows the full round-trip of a real-time order tracking feature: a domain event fired on the server, a private channel guarded by an authorization callback, and a front-end listener that updates the UI the moment an order changes state. It is the standard Laravel broadcasting pattern used when polling would be wasteful and users expect status like preparing or out_for_delivery to appear instantly.
The OrderStatusUpdated event implements ShouldBroadcast, which tells Laravel to push the event onto the broadcast connection instead of only dispatching it in-process. Because it also implements the queue contract implicitly through ShouldBroadcast, the actual serialization and transport happen on a queue worker, keeping the web request fast. broadcastOn() returns a PrivateChannel scoped to the specific order id, so each order gets its own isolated channel name. broadcastAs() renames the wire event to status.updated so the client subscribes to a stable, readable name rather than the fully-qualified class. broadcastWith() narrows the payload to just the fields the UI needs, which avoids leaking the whole model and keeps the message small. Note the SerializesModels trait plus a public $order — the model is rehydrated by id when the job runs, so it must still exist at broadcast time.
The OrderController demonstrates the trigger point. After persisting the new status inside a transaction, broadcast(new OrderStatusUpdated($order)) enqueues the event. Using broadcast() rather than event() makes the intent explicit and enables ->toOthers() when needed to skip the originating socket.
Private channels require authorization. In channels.php, the order.{orderId} callback runs during the /broadcasting/auth handshake; returning truthy authorizes the subscription. Here it confirms the authenticated user actually owns the order, which is the critical security boundary — without it any logged-in user could listen to any order.
On the client, useOrderStatus hook wraps Echo.private() in a React effect. It subscribes on mount, binds .listen('.status.updated') (the leading dot signals a custom broadcastAs name), and returns a cleanup that calls leaveChannel to prevent duplicate handlers and memory leaks across re-renders. The trade-off of this design is operational complexity: it needs a running queue worker and a websocket server (Pusher, Reverb, or a compatible driver), but it delivers genuinely push-based updates with per-resource authorization.
Related snips
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
{
"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
import React from "react";
type FallbackProps = {
error: Error;
reset: () => void;
};
React Error Boundary + error reporting hook
Share this code
Here's the card — post it anywhere.