---
sidebar_position: 8
---

# 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

```bash
composer require wordplus/realtime-php
npm install @wordplus/realtime
npx wordplus-realtime copy public/      # realtime-worker.js and realtime-rooms.min.js
```

```dotenv
WORDPLUS_SITE=s1500000007
WORDPLUS_SECRET=…
WORDPLUS_LOCAL_KEY=…              # openssl rand -hex 32
BROADCAST_CONNECTION=wordplus
```

```php
// 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`

```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](#profiles) name.

## Events

```php
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 ];
    }
}
```

```php
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](errors.md#publishing) 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:

```js
axios.defaults.headers.common[ 'X-Socket-ID' ] = rt.tab;   // or on your own fetch() calls
```

## The page

```blade
<head>
    @wordplusRealtime
    @vite( 'resources/js/app.js' )
</head>
```

```js
// 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()`:

```php
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](calls.md).

To publish without an event class:

```php
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()`:

```php
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](encryption.md#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:

```php
Realtime::rotateChannel( 'private-orders.42' );
Realtime::rotateUser( $user->id );
```

The keys' generations live in one of your cache stores, one that doesn't evict:

```dotenv
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](php-reference.md) is on it.
