---
sidebar_position: 12
---

# PHP reference: `wordplus/realtime-php`

The package needs PHP 8.1 or later, with sodium, and curl or PHP's streams. It takes the same options as the Node and Python packages, and answers the browser the same way. In Laravel, the `Realtime` facade and `app( Realtime::class )` are one instance, made from `config/wordplus-realtime.php` ([Laravel](laravel.md)).

## `new Realtime( $options )`

`Realtime::fromEnvironment( $options )` is the same, with `site`, `secret` and `local_key` taken from `WORDPLUS_SITE`, `WORDPLUS_SECRET` and `WORDPLUS_LOCAL_KEY`.

| Option | |
|---|---|
| `site`, `secret` | your app's key and secret |
| `local_key` | 64 hex characters of your own ([encryption](encryption.md#your-local-key)) |
| `profile` | `fn ( $user ) => [ 'name' => …, 'avatar' => …, 'url' => … ]` or `null`: the user's directory entry, and by default their presence info and call info |
| `has_keys` | `fn ( $user ) => false` cuts a user off every key |
| `generations` | a `GenerationStore`, for `rotateChannel()` and `rotateUser()` |
| `seal` | `false` sends everything plain |
| `base` | where your routes are, `/api/realtime` by default, for `config()` |
| `headers` | headers the browser sends to your routes, such as a CSRF token, for `config()` |
| `worker`, `rooms` | where your app serves the two files, for `config()` |
| `token_ttl`, `call_token_ttl` | seconds: 6 hours by default; a channels token lasts at most a day, a calls token at most 6 hours |
| `server`, `api` | WordPlus Cloud by default |
| `http` | `fn ( $method, $url, $headers, $body ) => [ 'status' => …, 'body' => … ]`, your own client for publishing |

A user is the id your app gives, an integer or a string, or `null` for nobody. In this version, calls and the directory take integer ids only.

## Owners

| Method | |
|---|---|
| `channel( $prefix, fn ( $user, $channel ) => $answer )` | private and presence channels, by the start of their names; answers `false`, `true`, or `[ 'id' => …, 'info' => … ]` ([channels](channels.md), [presence](presence.md)) |
| `calls( $prefix, fn ( $user, $call, $with ) => $answer )` | calls and rooms; answers `false`, `true`, or `[ 'info', 'publish', 'subscribe', 'admin', 'hidden' ]` ([calls](calls.md)) |
| `kind( $kind, fn ( $rest, $user, $label ) => bool, $newest = null )` | keys of your own kind ([encryption](encryption.md#your-own-sealed-values)) |
| `fallback( fn ( $user, $channel ) => $answer )` | the channels no prefix owns; in Laravel, `routes/channels.php` |

The longest prefix that fits decides. Each method returns the instance, so calls chain.

## Routes

| Method | |
|---|---|
| `respond( $action, $user )` | answers the current request in plain PHP: `auth`, `calls` or `keys`, with its status, headers and JSON |
| `answerRequest( $method, $contentType, $action, $body, $user )` | → `[ 'status', 'body' ]`, for a framework of your own |
| `handle( $action, $body, $user )` | → `[ 'status', 'body' ]`, for a body already decoded |

The routes take JSON POSTs only, and answer anything else with `415`. A page on another site can't send JSON without asking first, and it is never allowed to. A refusal is part of the answer (`denied`), not an HTTP error.

| Method | What it answers |
|---|---|
| `authorize( $channels, $user )` | `[ 'token', 'exp', 'keys', 'denied' ]` |
| `authorizeCall( $request, $user )` | `[ 'token', 'exp', 'key' ]` or `[ 'denied' => … ]` |
| `keysAnswer( $labels, $user )` | `[ 'keys', 'identity'? ]` |
| `identity( $user )` | `[ 'token', 'exp' ]` or `null` |
| `token( $channels, $user = null, $info = null, $ttl = null )` | a channels token, signed as the auth route signs |
| `config( $user = null, $headers = [] )` | the browser's settings for `connect()`, with a signed-in user's keys and identity |
| `mayJoin( $channel, $user )`, `mayHave( $label, $user )` | what the routes decide, for your own checks |

## Publishing

| Method | |
|---|---|
| `publish( $channels, $event, $data = null, [ 'except' => $tab, 'seal' => false ] )` | one channel, or up to 100; data up to 10 KB of JSON ([publishing](publishing.md)) |
| `publishBatch( [ [ 'channels', 'event', 'data', 'except', 'seal' ], … ] )` | 10 events a request |
| `channelInfo( $channel )` | `[ 'subscriptions', 'members'? ]` |
| `rotateChannel( $channel )`, `rotateUser( $user )` | moves a key on; needs `generations` |

They throw `RealtimeError` when WordPlus Cloud refuses: `$e->errorCode` holds the [code](errors.md#publishing), and `$e->status` the HTTP status.

## Keys

`$realtime->keys()` is the app's `Keys`:

| Method | |
|---|---|
| `seal( $text, $label )`, `sealJson( $data, $label )` | a sealed value, `{label}:1.{…}` |
| `open( $value )` | the text, or `null` |
| `entry( $label )` | a key as the browser takes it |
| `dayLabel()`, `channelLabel( $channel )`, `userLabel( $user )` | the labels the SDK uses |

## Generations

```php
interface GenerationStore
{
    public function get( string $name ): ?int;
    public function set( string $name, int $generation ): void;
}
```

Keep them where all your servers see them, and for good: a database row, or a cache that doesn't evict. In Laravel, `WordPlus\Realtime\Laravel\CacheGenerations` keeps them in a cache store.

## Helpers

`Realtime::channelKind( $channel )` gives `public`, `private` or `presence`. `Realtime::validChannel( $channel )` says whether WordPlus Cloud takes the name.
