Laravel
Laravel finds the package by itself, through package discovery. It brings:
- A
wordplusbroadcasting driver.broadcast( new OrderPaid( $order ) )publishes to the event's channels, sealed, androutes/channels.phpdecides who may listen, written as for any broadcaster. - The browser's routes:
POST /wordplus/realtime/auth,/callsand/keys, on thewebmiddleware, for the session's user. @wordplusRealtime: the page's settings, with the signed-in user's keys and the CSRF token.- The
Realtimefacade: calls, channels of your own, keys and publishing.
The browser listens with @wordplus/realtime, not Laravel Echo.
Setting up
composer require wordplus/realtime-php
npm install @wordplus/realtime
npx wordplus-realtime copy public/ # realtime-worker.js and realtime-rooms.min.js
WORDPLUS_SITE=s1500000007
WORDPLUS_SECRET=…
WORDPLUS_LOCAL_KEY=… # openssl rand -hex 32
BROADCAST_CONNECTION=wordplus
// config/broadcasting.php
'connections' => [
'wordplus' => [ 'driver' => 'wordplus' ],
// …
],
Laravel 11 and later leave out config/broadcasting.php and routes/channels.php until php artisan install:broadcasting adds them. You need neither Reverb nor Laravel Echo, so skip installing them.
Channels: routes/channels.php
Broadcast::channel( 'orders.{id}', fn ( User $user, string $id ) => $user->id === Order::find( $id )?->user_id );
// Presence: what the others see of the user, or false.
Broadcast::channel( 'room.{id}', fn ( User $user, string $id ) => $user->canEnter( $id ) ? [ 'name' => $user->name ] : false );
In the browser, a channel's name carries its kind, as it does in Pusher's:
| In Laravel | In the browser |
|---|---|
new PrivateChannel( 'orders.42' ) | private-orders.42 |
new PresenceChannel( 'room.7' ) | presence-room.7 |
new Channel( 'news' ) | news: public, for anyone with the app's key, never sealed |
Nobody signed in gets a private or presence channel, as with Laravel's own broadcasters. A presence callback that answers true shows the user's profile name.
Events
class OrderPaid implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct( public Order $order ) {}
public function broadcastOn(): array
{
return [ new PrivateChannel( 'orders.' . $this->order->id ) ];
}
public function broadcastAs(): string
{
return 'order.paid';
}
public function broadcastWith(): array
{
return [ 'status' => $this->order->status ];
}
}
broadcast( new OrderPaid( $order ) );
- The event's name is
broadcastAs()'s, or else its class (App\Events\OrderPaid). - Its data is
broadcastWith()'s, or else its public properties. It can be up to 10 KB of JSON, sealed with the channel's key. - Timing:
ShouldBroadcastgoes through your queue, andShouldBroadcastNowgoes at once. - Failures: a refusal from WordPlus Cloud throws Laravel's
BroadcastException, with the code in its message.
->toOthers() skips the tab whose request caused the event, when the page sends its tab as X-Socket-ID with its requests:
axios.defaults.headers.common[ 'X-Socket-ID' ] = rt.tab; // or on your own fetch() calls
The page
<head>
@wordplusRealtime
@vite( 'resources/js/app.js' )
</head>
// resources/js/app.js
import { connect } from '@wordplus/realtime';
const rt = await connect( window.wordplusRealtimeConfig );
rt.subscribe( 'private-orders.42' ).on( 'order.paid', ( data ) => refresh( data ) );
const room = rt.subscribe( 'presence-room.7' );
room.on( 'realtime:subscribed', () => draw( room.members() ) ); // [ { id, info: { name } } ]
@wordplusRealtime writes window.wordplusRealtimeConfig: where to connect, the routes with the CSRF token, and the signed-in user's keys, so the page connects with no request first.
Calls, and channels of your own
In a service provider's boot():
use WordPlus\Realtime\Laravel\Facades\Realtime;
// Calls with support: a customer with an agent, and the agents' own room.
Realtime::calls( 'support-', function ( $user, $call, $with ) {
$agent = fn ( $id ) => (bool) User::find( $id )?->is_agent;
return null === $with ? $agent( $user ) : $agent( $user ) || $agent( $with );
} );
// A channel beside routes/channels.php, by the start of its name.
Realtime::channel( 'private-staff-', fn ( $user ) => (bool) User::find( $user )?->is_staff );
$userand$withare ids here, not models.$withis the other person of a call between two, ornullfor a room.- Which decides: a channel that a
Realtime::channel()prefix owns is decided there.routes/channels.phpdecides the rest. - More: what a call's answer may hold, and the call screen, are in calls.
To publish without an event class:
Realtime::publish( 'private-orders.42', 'updated', [ 'status' => 'paid' ] );
Profiles
What others see of a user, in presence, calls and the directory, is their name. It includes their avatar too, or Jetstream's profile_photo_url, when they have one. To say otherwise, in a service provider's boot():
use WordPlus\Realtime\Laravel\RealtimeServiceProvider;
RealtimeServiceProvider::profileUsing( function ( $id ) {
$user = User::find( $id );
return $user ? [ 'name' => $user->display_name, 'avatar' => $user->avatar_url ] : null;
} );
A profile is sealed with the day's key. Pages read others' with rt.users( [ … ] ) (your users).
Rotating keys
When someone loses access to a channel, or to everything, move the key on, so what comes next is sealed from them:
Realtime::rotateChannel( 'private-orders.42' );
Realtime::rotateUser( $user->id );
The keys' generations live in one of your cache stores, one that doesn't evict:
WORDPLUS_REALTIME_GENERATIONS=redis # or database
The config
php artisan vendor:publish --tag=wordplus-realtime copies it to config/wordplus-realtime.php.
| Key | Default | |
|---|---|---|
site, secret, local_key | WORDPLUS_SITE, WORDPLUS_SECRET, WORDPLUS_LOCAL_KEY | |
routes.prefix | wordplus/realtime | where the browser's routes are |
routes.middleware | ['web'] | how the routes know the user |
routes.enabled | true | false to register RealtimeController yourself |
worker, rooms | /realtime-worker.js, /realtime-rooms.min.js | where your app serves the two files |
generations | WORDPLUS_REALTIME_GENERATIONS | the cache store for rotating keys |
seal | true | false sends everything plain |
broadcasting | wordplus | the connection whose routes/channels.php decides |
server, api | WordPlus Cloud | WORDPLUS_REALTIME_SERVER, WORDPLUS_REALTIME_API |
The facade is the WordPlus\Realtime\Realtime singleton. Everything in the PHP reference is on it.