Laravel Biosphere is a third-party package for realtime bi-directional communication between a server and a client.
Laravel Reverb is a nice package, however it is not truly bi-directional out of the box. Server can send messages to clients, clients can 'whisper' messages to other clients, however you cannot send a message from a client to a server without some ugly workarounds, such as using an underlying Echo backend, which can (and will) eventually break on backend change.
On the other hand, Biosphere is a true WebSocket solution, supporting sending messages both from a client, and from a server.
Biosphere consists of three parts:
- A client that can listen, and emit events;
- A proxy that passes events back and forth between the client, and the server;
- And a server that, like the client, can listen, and emit events.
Why proxy, you ask? Implementing WebSocket server in PHP is slow for users, and pain for programmers. Using a proxy powered by Bun accelerates the exchange speed, keeping developers in the cozy warmth offered by Laravel.
To start using Laravel Biosphere, install its parts:
composer require laravel-biosphere/server
npm install @laravel-biosphere/proxy
npm install @laravel-biosphere/clientAlso, to run proxy, you will need Bun:
# Linux, MacOS
curl -fsSL https://bun.sh/install | bash# ...sigh, and Windows
powershell -c "irm bun.sh/install.ps1 | iex"Before running Biosphere, you need to configure it. It's all done in your .env, copy the following, and follow the instructions:
# Used by the proxy to authorize clients on channels;
# We will tell you more about channels later;
# If you run Laravel without Docker, use `localhost` for the domain,
# Otherwise, use you container's name in your Docker network;
BIOSPHERE_LARAVEL_AUTHORIZE_URL=http://<your domain>/biosphere/authorize
# Used by clients to request personal short lived tokens;
# These tokens are requested by a client automatically, and used to authenticate clients in the proxy;
# Laravel generates a token, sends it to the client, and the client can use it during channel authorization,
# for the proxy to ensure that this is a real, trusted request;
BIOSPHERE_LARAVEL_NEW_TOKEN_URL=http://<localhost OR Docker domain>:3000/biosphere/new-token
# These are the names of channels we use in Redis pub/sub;
# You are unlikely to change those, unless it somehow collides with your Redis pub/sub channels;
BIOSPHERE_REDIS_CHANNEL_TO_SERVER=biosphere:to-server
BIOSPHERE_REDIS_CHANNEL_FROM_SERVER=biosphere:from-server
# The server will check the proxy requests using this token;
# This is a secret, do not compromise!
# Use any random string that would be hard to generate;
BIOSPHERE_TOKEN=<any random string>Biosphere needs to run 2 more processes in your system - the proxy, and the client; To run the proxy, use:
bun run ./node_modules/@laravel-biosphere/proxy/src/main.ts & disownAnd to run the server, use:
php artisan biosphere:serveTip
If you have experience in Laravel Reverb or Socket.io, we use the same terminology, such as channels!
In Biosphere, you can define channels. Channels have names, and users can connect to, and disconnect from them. Both clients, and the server can send events; Events can carry any information you want. For example, if you are making a trading platform, you may wish to emit a price change event from the servers, so everyone stays in sync pricewise. If event is emitted by a client, it's heard only by the server; If event is emitted by the server, it's heard by all the clients connected to the channel;
Let's begin our journey by creating a class for a channel.
Tip
I stick to Laravel Reverb's guidelines, so in Biosphere
it's recommended for your channel classes to be in App\Broadcasting\ namespace,
and have XxxChannel.php name.
<?php
namespace App\Broadcasting;
use Anafro\Biosphere\Channels\Channel;
use Anafro\Biosphere\Messages\Message;
use App\Rooms\Room;
use Illuminate\Support\Facades\Log;
class ChatChannel extends Channel
{
public function __construct(string $pattern, string $name)
{
parent::__construct($pattern, $name);
}
/**
* @param \App\Models\User $user
*/
public function authorize(mixed $user): bool
{
//
}
/**
* @param \App\Models\User $user
*/
public function connected(mixed $user): void
{
//
}
/**
* @param \App\Models\User $user
*/
public function disconnected(mixed $user): void
{
//
}
public function message(Message $message): void
{
//
}
/**
* @param \App\Models\User $user
*/
public function heartbeat(mixed $user): void
{
//
}
}authorizedecides whether a client is allowed to connect to this channel, or not; It works likeauthorizemethod inFormRequests.connectedanddisconnectedare self-explanatory - run when clients connect/disconnect;messageis run when this channel receives a message.heartbeatis run when this channel receives apongevent. I will talk about heartbeat later; Worth to mention now that for most cases it's never used;
Before connecting, we can authorize users.
To do that, implement the authorize method.
Return true to allow connection, false to deny. Simple!
Here are some examples:
/**
* @param \App\Models\User $user
*/
public function authorize(mixed $user): bool
{
// Only admins allowed
return $user->isAdmin;
}/**
* @param \App\Models\User $user
*/
public function authorize(mixed $user): bool
{
// If you return true, everyone can join the channel.
// In Reverb world, such channels are called 'public'.
return true;
}/**
* @param \App\Models\User $user
*/
public function authorize(mixed $user): bool
{
// You can even ignore $user at all,
// and depend authorization on something else!
// E.g. you can connect to this channel in January
// whoever you are.
return Carbon::now()->month === 1;
}Any event is received in the message method.
A message consists of:
- The name of the event (
$message->event); - The event-related data (
$message->data); - The user that sent this message (
$message->user);
By checking the event name, you can handle separate events as you wish. For example:
public function message(Message $message): void
{
switch ($message->event) {
case 'like':
$username = $message->user->name;
$video = $message->data['video'];
Log::info("$username left a like on video $video <3!");
break;
case '...':
...
}
}You can send events from the server. To do so, you can use $this->send inside channel class.
It will send the message to this channel.
send method accepts two arguments:
- An event name to send to this channel;
- And a event data array.
If your event has no data, you can omit the second arguments.
For example:
public function message(Message $message): void
{
switch ($message->event) {
case 'like':
$username = $message->user->name;
$video = $message->data['video'];
// It's a common scenario to let the other people know
// that the event has happened.
// A client likes a video, that events is sent here,
// and effectively is sent back to all users,
// including the person who sent the event;
// You can filter out that event on client by username for example,
// so clients ignore `liked` event emitted by themselves,
// or actually wait for `liked` event to arrive to ensure
// that the video was liked for real;
$this->send('liked', compact('username', 'video'));
break;
case '...':
...
}
}If WebSocket connection is unused for some period of time (likely for a minute), it closes.
To prevent connection close, Biosphere uses a heartbeat system.
Every 3 seconds, the server sends a ping event to all the channels.
Clients connected to channels respond with a pong event.
pongs trigger the heartbeat method instead of message.
Warning
This feature is just a possibility left for unusual usages, and I cannot provide useful examples for this.
To make channels available, use Biosphere::channel in routes/channels.php:
<?php
use App\Broadcasting\ChatChannel;
use App\Models\User;
use Anafro\Biosphere\Facades\Biosphere;
Biosphere::channel("/Chat/", ChatChannel::class);Channel names can have parameters. It's useful if you have multiple channels with the same behavior,
for example - chats! You can have multiple chats in your projects with their own names.
Think of it like parameters in web routes, but instead of having /chat/{name}, we have:
Biosphere::channel("/Chat #(?<name>[a-zA-Z0-9-_]+)/", ChatChannel::class);Tip
If you don't know about ?<> syntax in regex, read Mozilla docs.
tl;dr: it gives regex match group names.
In any channel method, you can use $this->parameter method:
public function message(Message $message): void
{
switch ($message->event) {
case 'chat message':
// If your channel name was "Chat #cool_chat_1337",
// the following will be 'cool_chat_1337':
$chatName = $this->parameter('name');
$this->send('liked', compact('username', 'video'));
break;
case '...':
...
}
}You can schedule event dispatch by using $this->schedule inside a channel.
public function message(Message $message): void
{
switch ($message->event) {
case 'timer':
// ID is for cancellation - IDs must be unique, so we suggest
// using something like UUIDs;
// Read about cancellation below.
$id = Str::uuid();
$this->schedule($id, 'ding-dong', 60000, [
'message' => 'Time\'s up!',
]);
break;
case '...':
...
}
}Warning
Events sent via schedule method are sent back to the server!
Use message method to handle them!
Warning
Messages triggered via schedule won't have user!
You can cancel scheduled event by call $this->cancel($id):
public function message(Message $message): void
{
switch ($message->event) {
case 'stop timer':
// You need an ID to cancel the specific scheduled event.
// You might want to send the ID to the user,
// or store it in Redis, you name it!
$id = $message->data['id'];
$this->cancel($id);
break;
case '...':
...
}
}[!INFO] Cancellation of non-existent scheduled, or already emitted events is silently ignored.
To connect to Biosphere, import and use Biosphere client:
import { biosphere } from "@laravel-biosphere/client";
const chat = await biosphere.channel("Chat #cool_chat_1337");To listen, use on method of a channel object:
import { biosphere } from "@laravel-biosphere/client";
const chat = await biosphere.channel("Chat #cool_chat_1337");
chat.on(/chat message/, message => {
console.info(`New chat message from ${message.username}: ${message.text}`);
// It would receive $this->send('chat message', compact('username', 'text')) from the server.
});To stop listening, use off:
chat.off(/chat message/);Warning
.off does not match listener regexes, but strictly compares them.
So, .off(/.*/) WILL NOT remove all listeners as you could expect.
Instead, it will remove listeners that was exactly declared as .on(/.*/).
To stop listening to all events, use .offAll().
To emit an event to a server, use .send. It is identical to the server's one:
chat.send(/chat message/, {
text: 'Yooo, hows that goiinnng tiger?',
});[!INFO] User is added to the message by the proxy, so you must not include
userIdas in data array.
To close the connection, yep, use .close():
chat.close();Warning
Channels cannot be reopened.
Instead, create another channel object via await biosphere.channel(name).
MIT, except for everything related to Laravel team. Please, consult their README to know about their license.