laravel-websockets/src/Websocket/Controller.php

401 lines
11 KiB
PHP
Raw Normal View History

2025-01-16 07:54:02 +00:00
<?php
declare(strict_types=1);
namespace BlaxSoftware\LaravelWebSockets\Websocket;
use BlaxSoftware\LaravelWebSockets\ChannelManagers\LocalChannelManager;
use BlaxSoftware\LaravelWebSockets\ChannelManagers\RedisChannelManager;
use BlaxSoftware\LaravelWebSockets\Channels\Channel;
use BlaxSoftware\LaravelWebSockets\Channels\PresenceChannel;
use BlaxSoftware\LaravelWebSockets\Channels\PrivateChannel;
use Ratchet\ConnectionInterface;
2025-01-17 09:45:53 +00:00
use Illuminate\Support\Facades\Log;
2025-01-16 07:54:02 +00:00
class Controller
{
2025-12-05 21:08:47 +00:00
protected bool $isMockConnection;
protected ?MockConnection $mockConnectionClone = null;
2025-01-16 07:54:02 +00:00
final public function __construct(
protected ConnectionInterface $connection,
protected PrivateChannel|Channel|PresenceChannel|null $channel,
protected string $event,
protected LocalChannelManager|RedisChannelManager $channelManager
2025-12-05 21:08:47 +00:00
) {
// Cache class check to avoid repeated get_class() calls (reflection is slow)
$this->isMockConnection = get_class($connection) === MockConnection::class;
// Pre-clone MockConnection once if needed (reuse across method calls)
if ($this->isMockConnection) {
$this->mockConnectionClone = clone $connection;
}
}
2025-01-16 07:54:02 +00:00
2025-12-17 20:34:20 +00:00
/**
* To be overridden by child classes if needed
* Called before need_auth check
*
* @return void
*/
public function boot(): void {}
/**
* To be overridden by child classes if needed
* Called after need_auth check
*
* @return void
*/
public function booted(): void {}
/**
* To be overridden by child classes if needed
* Called after main function execution (even if not found)
*
* @return void
*/
public function unboot(): void {}
2025-01-16 07:54:02 +00:00
public static function controll_message(
ConnectionInterface $connection,
2025-09-14 13:00:27 +00:00
PrivateChannel|Channel|PresenceChannel $channel,
2025-01-16 07:54:02 +00:00
array $message,
LocalChannelManager|RedisChannelManager $channelManager
) {
$event = self::get_event($message);
2025-12-05 21:08:47 +00:00
if (count($event) !== 2) {
return self::send_error($connection, $message, 'Event unknown');
2025-01-16 07:54:02 +00:00
}
try {
2025-01-17 10:06:30 +00:00
$contr = (strpos($event[0], '-') >= 0)
2025-12-05 21:08:47 +00:00
? implode('', array_map(fn($item) => ucfirst($item), explode('-', $event[0])))
2025-01-16 07:54:02 +00:00
: ucfirst($event[0]);
2025-12-05 21:08:47 +00:00
$vendorcontroller = '\\BlaxSoftware\\LaravelWebSockets\\Websocket\\Controllers\\' . $contr . 'Controller';
$appcontroller = '\\App\\Websocket\\Controllers\\' . $contr . 'Controller';
2025-01-16 07:54:02 +00:00
$method = static::without_uniquifyer($event[1]);
2025-12-05 21:08:47 +00:00
$controllerClass = class_exists($appcontroller)
2025-01-17 10:06:30 +00:00
? $appcontroller
2025-12-05 21:08:47 +00:00
: (class_exists($vendorcontroller) ? $vendorcontroller : null);
if (! $controllerClass) {
return self::send_error($connection, $message, 'Event could not be associated');
2025-01-16 07:54:02 +00:00
}
2025-12-05 21:08:47 +00:00
$controller = new $controllerClass(
2025-01-16 07:54:02 +00:00
$connection,
$channel,
$message['event'],
$channelManager
2025-12-05 21:08:47 +00:00
);
2025-01-16 07:54:02 +00:00
2025-12-17 20:34:20 +00:00
$controller->boot();
2025-01-16 07:54:02 +00:00
if (($controller->need_auth ?? true) && ! $connection->user) {
2025-12-17 20:34:20 +00:00
$e = $controller->error('Unauthorized');
$controller->unboot();
return $e;
2025-01-16 07:54:02 +00:00
}
2025-12-17 20:34:20 +00:00
if (! method_exists($controllerClass, $method)) {
$e = self::send_error($connection, $message, 'Event could not be handled');
$controller->unboot();
return $e;
}
$controller->booted();
2025-01-16 07:54:02 +00:00
$payload = $controller->$method(
$connection,
2025-06-18 09:02:39 +00:00
@$message['data'] ?? [],
2025-01-16 07:54:02 +00:00
$message['channel']
);
2025-12-17 20:34:20 +00:00
$controller->unboot();
2025-12-05 21:08:47 +00:00
if ($payload === false || $payload === true) {
return null;
2025-01-16 07:54:02 +00:00
}
$connection->send(json_encode([
'event' => $message['event'] . ':response',
'data' => $payload,
'channel' => $message['channel'],
]));
return $payload;
} catch (\Throwable $e) {
2025-01-16 07:54:02 +00:00
$reload = [
'event' => @$message['event'],
'data' => @$message['data'],
'channel' => @$message['channel'],
'line' => $e->getFile() . ':' . $e->getLine(),
2025-09-07 06:58:09 +00:00
'stack' => $e->getTraceAsString(),
2025-01-16 07:54:02 +00:00
];
2025-01-17 09:45:53 +00:00
Log::error($e->getMessage(), $reload);
2025-01-16 07:54:02 +00:00
2025-12-05 21:08:47 +00:00
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
2025-01-16 07:54:02 +00:00
2025-12-05 21:08:47 +00:00
return self::send_error($connection, $message, $e->getMessage(), true);
}
2025-01-16 07:54:02 +00:00
}
2025-01-20 15:46:13 +00:00
final public function progress(
mixed $payload = null,
?string $event = null,
?string $channel = null
2025-12-05 21:08:47 +00:00
): bool {
2025-01-20 15:46:13 +00:00
$p = [
'event' => ($event ?? $this->event) . ':progress',
'data' => $payload,
'channel' => $channel ?? $this->channel->getName(),
];
// if payload only contains key "data"
if (
count($p) === 1
&& isset($payload['data'])
) {
$p['data'] = $payload['data'];
}
2025-12-05 21:08:47 +00:00
// Pre-encode once (avoid repeated encoding)
$encoded = json_encode($p);
if ($this->isMockConnection) {
$this->mockConnectionClone->send($encoded);
2025-01-20 15:46:13 +00:00
} else {
2025-12-05 21:08:47 +00:00
$this->connection->send($encoded);
2025-01-20 15:46:13 +00:00
}
return true;
}
2025-01-16 07:54:02 +00:00
final public function success(
mixed $payload = null,
?string $event = null,
?string $channel = null
2025-12-05 21:08:47 +00:00
): bool {
2025-01-16 07:54:02 +00:00
$p = [
'event' => ($event ?? $this->event) . ':response',
'data' => $payload,
'channel' => $channel ?? $this->channel->getName(),
];
// if payload only contains key "data"
if (
count($p) === 1
&& isset($payload['data'])
) {
$p['data'] = $payload['data'];
}
2025-12-05 21:08:47 +00:00
// Pre-encode once (avoid repeated encoding)
$encoded = json_encode($p);
if ($this->isMockConnection) {
$this->mockConnectionClone->send($encoded);
2025-01-16 07:54:02 +00:00
} else {
2025-12-05 21:08:47 +00:00
$this->connection->send($encoded);
2025-01-16 07:54:02 +00:00
}
return true;
}
final public function error(
array|string|null $payload = null,
?string $event = null,
?string $channel = null
2025-12-05 21:08:47 +00:00
): bool {
2025-01-16 07:54:02 +00:00
if (is_string($payload)) {
$payload = [
'message' => $payload,
];
}
$p = [
'event' => ($event ?? $this->event) . ':error',
'data' => $payload,
'channel' => $channel ?? $this->channel->getName(),
];
// if payload only contains key "data"
if (
count($p) === 1
&& isset($payload['data'])
) {
$p['data'] = $payload['data'];
}
// get line from where this is called from
$trace = debug_backtrace();
$p['data']['trace'] = $trace
? $trace[0]['line']
: null;
2025-06-13 08:31:59 +00:00
Log::channel('websocket')->error('Send error: ' . @$p['data']['message'], $p);
2025-01-16 07:54:02 +00:00
2025-12-05 21:08:47 +00:00
// Pre-encode once (avoid repeated encoding)
$encoded = json_encode($p);
if ($this->isMockConnection) {
$this->mockConnectionClone->send($encoded);
2025-01-16 07:54:02 +00:00
} else {
2025-12-05 21:08:47 +00:00
$this->connection->send($encoded);
2025-01-16 07:54:02 +00:00
}
return true;
}
2025-09-13 17:48:59 +00:00
final public function broadcast(
2025-09-13 17:33:29 +00:00
array|string|null $payload = null,
?string $event = null,
?string $channel = null,
bool $including_self = false
2025-12-05 21:08:47 +00:00
) {
2025-09-13 17:33:29 +00:00
if (is_string($payload)) {
$payload = [
'message' => $payload,
];
}
2025-09-13 17:48:59 +00:00
$channel ??= ($this->channel ? $this->channel->getName() : null);
2025-09-13 17:33:29 +00:00
$p = [
'event' => ($event ?? $this->event),
'data' => $payload,
2025-09-13 17:48:59 +00:00
'channel' => $channel,
2025-09-13 17:33:29 +00:00
];
2025-12-05 21:08:47 +00:00
if (!$this->isMockConnection) {
2025-09-13 17:48:59 +00:00
if (! $channel) {
$this->error('Channel not found');
return;
}
2025-12-05 21:08:47 +00:00
// Pre-encode ONCE - massive improvement for 100+ connections
$encoded = json_encode($p);
2025-09-13 17:48:59 +00:00
foreach ($this->channel->getConnections() as $channel_conection) {
if ($channel_conection !== $this->connection) {
2025-12-05 21:08:47 +00:00
$channel_conection->send($encoded);
2025-09-13 17:48:59 +00:00
}
if ($including_self) {
2025-12-05 21:08:47 +00:00
$this->connection->send($encoded);
2025-09-13 17:48:59 +00:00
}
}
2025-12-05 21:08:47 +00:00
} else {
$this->mockConnectionClone->broadcast(
2025-09-13 17:48:59 +00:00
$p,
$channel,
$including_self
);
2025-09-13 17:33:29 +00:00
}
2025-09-15 12:29:07 +00:00
}
final public function whisper(
array|string|null $payload = null,
?string $event = null,
array $socketIds,
?string $channel = null
2025-12-05 21:08:47 +00:00
) {
2025-09-15 12:29:07 +00:00
if (is_string($payload)) {
$payload = [
'message' => $payload,
];
}
$channel ??= ($this->channel ? $this->channel->getName() : null);
$p = [
'event' => ($event ?? $this->event),
'data' => $payload,
'channel' => $channel,
];
2025-09-13 17:33:29 +00:00
2025-12-05 21:08:47 +00:00
if (!$this->isMockConnection) {
2025-09-15 12:29:07 +00:00
if (! $channel) {
$this->error('Channel not found');
return;
}
2025-12-05 21:08:47 +00:00
// Pre-encode ONCE for all matching sockets
$encoded = json_encode($p);
// Use array_flip for O(1) lookup instead of O(n) in_array
$socketIdLookup = array_flip($socketIds);
2025-09-15 12:29:07 +00:00
foreach ($this->channel->getConnections() as $channel_conection) {
2025-12-05 21:08:47 +00:00
if (isset($socketIdLookup[$channel_conection->socketId])) {
$channel_conection->send($encoded);
2025-09-15 12:29:07 +00:00
}
}
2025-12-05 21:08:47 +00:00
} else {
$this->mockConnectionClone->whisper(
2025-09-15 12:29:07 +00:00
$p,
$socketIds,
$channel
);
}
2025-09-13 17:33:29 +00:00
}
2025-12-05 21:08:47 +00:00
private static function send_error(
ConnectionInterface $connection,
array $message,
string $reason,
bool $reported = false
) {
$connection->send(json_encode([
'event' => ($message['event'] ?? 'unknown') . ':error',
'data' => [
'message' => $reason,
'meta' => [
'reported' => $reported,
],
],
'channel' => $message['channel'] ?? null,
]));
return null;
}
2025-01-16 07:54:02 +00:00
protected static function get_uniquifyer($event)
{
preg_match('/[\[].*[\]]/', $event, $matches);
if (count($matches) === 1) {
$uniqiueifier = $matches[0];
}
return $uniqiueifier ?? null;
}
protected static function without_uniquifyer($event)
{
return preg_replace('/[\[].*[\]]/', '', $event);
}
private static function get_event($message)
{
$event = explode('.', $message['event']);
2025-01-18 16:06:52 +00:00
if (strpos($event[0], 'pusher.') > -1) {
$event = explode('.', $event[0]);
}
2025-01-16 07:54:02 +00:00
if (strpos($event[0], 'pusher:') > -1) {
$event = explode(':', $event[0]);
}
return $event;
}
}