Skip to main content
Markdown

Laravel

Laravel finds the package by itself, through package discovery. It brings:

  • A wordplus broadcasting driver. broadcast( new OrderPaid( $order ) ) publishes to the event's channels, sealed, and routes/channels.php decides who may listen, written as for any broadcaster.
  • The browser's routes: POST /wordplus/realtime/auth, /calls and /keys, on the web middleware, for the session's user.
  • @wordplusRealtime: the page's settings, with the signed-in user's keys and the CSRF token.
  • The Realtime facade: 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 LaravelIn 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: ShouldBroadcast goes through your queue, and ShouldBroadcastNow goes 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 );
  • $user and $with are ids here, not models.
  • $with is the other person of a call between two, or null for a room.
  • Which decides: a channel that a Realtime::channel() prefix owns is decided there. routes/channels.php decides 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.

KeyDefault
site, secret, local_keyWORDPLUS_SITE, WORDPLUS_SECRET, WORDPLUS_LOCAL_KEY
routes.prefixwordplus/realtimewhere the browser's routes are
routes.middleware['web']how the routes know the user
routes.enabledtruefalse to register RealtimeController yourself
worker, rooms/realtime-worker.js, /realtime-rooms.min.jswhere your app serves the two files
generationsWORDPLUS_REALTIME_GENERATIONSthe cache store for rotating keys
sealtruefalse sends everything plain
broadcastingwordplusthe connection whose routes/channels.php decides
server, apiWordPlus CloudWORDPLUS_REALTIME_SERVER, WORDPLUS_REALTIME_API

The facade is the WordPlus\Realtime\Realtime singleton. Everything in the PHP reference is on it.