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;
|
2026-02-09 12:04:22 +00:00
|
|
|
protected MockConnectionSocketPair|null $mockConnectionClone = null;
|
2025-12-05 21:08:47 +00:00
|
|
|
|
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)
|
2026-02-02 10:24:54 +00:00
|
|
|
$connectionClass = get_class($connection);
|
2026-02-09 12:04:22 +00:00
|
|
|
$this->isMockConnection = $connectionClass === MockConnectionSocketPair::class;
|
2025-12-05 21:08:47 +00:00
|
|
|
|
|
|
|
|
// 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
|
2025-12-18 08:29:34 +00:00
|
|
|
* If return is exactly false, processing stops
|
2025-12-17 20:34:20 +00:00
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
2025-12-18 08:29:34 +00:00
|
|
|
public function boot() {}
|
2025-12-17 20:34:20 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* To be overridden by child classes if needed
|
|
|
|
|
* Called after need_auth check
|
2025-12-18 08:29:34 +00:00
|
|
|
* If return is exactly false, processing stops
|
2025-12-17 20:34:20 +00:00
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
2025-12-18 08:29:34 +00:00
|
|
|
public function booted() {}
|
2025-12-17 20:34:20 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 {
|
2026-01-24 13:42:35 +00:00
|
|
|
$eventPrefix = $event[0];
|
2025-01-16 07:54:02 +00:00
|
|
|
$method = static::without_uniquifyer($event[1]);
|
|
|
|
|
|
2026-01-24 13:42:35 +00:00
|
|
|
// Use cached controller resolver for fast lookup
|
|
|
|
|
$controllerClass = ControllerResolver::resolve($eventPrefix);
|
2025-12-05 21:08:47 +00:00
|
|
|
|
|
|
|
|
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-18 08:29:34 +00:00
|
|
|
if ($controller->boot() === false) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-17 20:34:20 +00:00
|
|
|
|
2025-01-16 07:54:02 +00:00
|
|
|
if (($controller->need_auth ?? true) && ! $connection->user) {
|
2025-12-18 08:29:34 +00:00
|
|
|
$controller->error('Unauthorized');
|
2025-12-17 20:34:20 +00:00
|
|
|
$controller->unboot();
|
2025-12-18 08:29:34 +00:00
|
|
|
return;
|
2025-01-16 07:54:02 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 20:34:20 +00:00
|
|
|
if (! method_exists($controllerClass, $method)) {
|
2025-12-18 08:29:34 +00:00
|
|
|
$controller->error($connection, $message, 'Event could not be handled');
|
2025-12-17 20:34:20 +00:00
|
|
|
$controller->unboot();
|
2025-12-18 08:29:34 +00:00
|
|
|
return;
|
2025-12-17 20:34:20 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-18 08:29:34 +00:00
|
|
|
if ($controller->booted() === false) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-17 20:34:20 +00:00
|
|
|
|
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;
|
2025-09-06 08:43:07 +00:00
|
|
|
} 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) {
|
|
|
|
|
// 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);
|
2026-02-02 10:24:54 +00:00
|
|
|
$sentTo = [];
|
|
|
|
|
|
|
|
|
|
// Search ALL connections across ALL channels to find target socket IDs
|
|
|
|
|
// This is necessary because whisper targets specific sockets regardless of channel
|
|
|
|
|
$this->channelManager->getLocalConnections()->then(function ($connections) use ($socketIdLookup, $encoded, &$sentTo) {
|
|
|
|
|
foreach ($connections as $connection) {
|
|
|
|
|
// Skip if already sent to this socket (can appear in multiple channels)
|
|
|
|
|
if (isset($sentTo[$connection->socketId])) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isset($socketIdLookup[$connection->socketId])) {
|
|
|
|
|
$connection->send($encoded);
|
|
|
|
|
$sentTo[$connection->socketId] = true;
|
|
|
|
|
}
|
2025-09-15 12:29:07 +00:00
|
|
|
}
|
2026-02-02 10:24:54 +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;
|
|
|
|
|
}
|
|
|
|
|
}
|