2025-05-08 08:54:11 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace BlaxSoftware\LaravelWebSockets\Services;
|
|
|
|
|
|
2026-02-03 14:03:50 +00:00
|
|
|
use BlaxSoftware\LaravelWebSockets\Broadcast\BroadcastClient;
|
|
|
|
|
|
2025-05-08 08:54:11 +00:00
|
|
|
class WebsocketService
|
|
|
|
|
{
|
2026-02-03 14:03:50 +00:00
|
|
|
/**
|
|
|
|
|
* Send a message via WebSocket.
|
|
|
|
|
*
|
|
|
|
|
* Automatically uses the efficient Unix socket broadcast when available,
|
|
|
|
|
* falling back to creating a new WebSocket connection when not.
|
2026-02-03 14:38:57 +00:00
|
|
|
*
|
|
|
|
|
* Supports the legacy 'app.whisp' pattern where data contains:
|
|
|
|
|
* - 'event': The actual event name to broadcast
|
|
|
|
|
* - 'data': The actual data payload
|
|
|
|
|
* - 'sockets': Target socket IDs (optional)
|
|
|
|
|
* - 'channel': Target channel (optional)
|
2026-02-03 14:03:50 +00:00
|
|
|
*/
|
2025-09-15 12:29:07 +00:00
|
|
|
public static function send(
|
|
|
|
|
string $event,
|
|
|
|
|
mixed $data,
|
|
|
|
|
$channel = 'websocket'
|
2026-02-03 14:03:50 +00:00
|
|
|
) {
|
|
|
|
|
// Try efficient broadcast socket first (Unix socket IPC)
|
|
|
|
|
if (ws_available()) {
|
2026-02-03 14:38:57 +00:00
|
|
|
// Handle legacy 'app.whisp' pattern - extract inner event, data, and sockets
|
|
|
|
|
if ($event === 'app.whisp' && is_array($data)) {
|
|
|
|
|
$innerEvent = $data['event'] ?? 'info:message';
|
|
|
|
|
$innerData = $data['data'] ?? [];
|
|
|
|
|
$innerChannel = $data['channel'] ?? $channel ?? 'websocket';
|
|
|
|
|
$targetSockets = $data['sockets'] ?? null;
|
|
|
|
|
|
|
|
|
|
if (!empty($targetSockets) && is_array($targetSockets)) {
|
|
|
|
|
// Whisper to specific sockets
|
|
|
|
|
$success = ws_whisper($innerEvent, $innerData, $targetSockets, $innerChannel);
|
|
|
|
|
} else {
|
|
|
|
|
// Broadcast to all
|
|
|
|
|
$success = ws_broadcast($innerEvent, $innerData, $innerChannel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($success) {
|
|
|
|
|
return (object)['success' => true, 'method' => 'broadcast_socket'];
|
|
|
|
|
}
|
|
|
|
|
// Fall through to WebSocket client if broadcast socket fails
|
|
|
|
|
} else {
|
|
|
|
|
// Regular broadcast
|
|
|
|
|
$success = ws_broadcast($event, is_array($data) ? $data : ['data' => $data], $channel ?? 'websocket');
|
|
|
|
|
if ($success) {
|
|
|
|
|
return (object)['success' => true, 'method' => 'broadcast_socket'];
|
|
|
|
|
}
|
|
|
|
|
// Fall through to WebSocket client if broadcast socket fails
|
2026-02-03 14:03:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback: Create new WebSocket connection (slower, for when broadcast socket not available)
|
|
|
|
|
return static::sendViaWebSocket($event, $data, $channel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send a message to specific socket IDs only.
|
|
|
|
|
*
|
|
|
|
|
* @param string $event Event name
|
|
|
|
|
* @param mixed $data Event data
|
|
|
|
|
* @param array $sockets Target socket IDs
|
|
|
|
|
* @param string $channel Channel name
|
|
|
|
|
* @return bool Success
|
|
|
|
|
*/
|
|
|
|
|
public static function whisper(
|
|
|
|
|
string $event,
|
|
|
|
|
mixed $data,
|
|
|
|
|
array $sockets,
|
|
|
|
|
string $channel = 'websocket'
|
|
|
|
|
): bool {
|
|
|
|
|
if (!ws_available()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ws_whisper($event, is_array($data) ? $data : ['data' => $data], $sockets, $channel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Broadcast to all except specified socket IDs.
|
|
|
|
|
*
|
|
|
|
|
* @param string $event Event name
|
|
|
|
|
* @param mixed $data Event data
|
|
|
|
|
* @param array $excludeSockets Socket IDs to exclude
|
|
|
|
|
* @param string $channel Channel name
|
|
|
|
|
* @return bool Success
|
|
|
|
|
*/
|
|
|
|
|
public static function broadcastExcept(
|
|
|
|
|
string $event,
|
|
|
|
|
mixed $data,
|
|
|
|
|
array $excludeSockets,
|
|
|
|
|
string $channel = 'websocket'
|
|
|
|
|
): bool {
|
|
|
|
|
if (!ws_available()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ws_broadcast_except($event, is_array($data) ? $data : ['data' => $data], $excludeSockets, $channel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send a message by creating a new WebSocket connection.
|
|
|
|
|
* This is the legacy method, kept for fallback when broadcast socket is unavailable.
|
|
|
|
|
*/
|
|
|
|
|
protected static function sendViaWebSocket(
|
|
|
|
|
string $event,
|
|
|
|
|
mixed $data,
|
|
|
|
|
$channel = 'websocket'
|
2025-09-15 12:29:07 +00:00
|
|
|
) {
|
2026-02-03 14:38:57 +00:00
|
|
|
try {
|
|
|
|
|
$client = new \WebSocket\Client('ws://0.0.0.0:6001/app/' . config('websockets.apps.0.id'), [
|
|
|
|
|
'timeout' => 5,
|
|
|
|
|
'headers' => [],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Read connection_established
|
|
|
|
|
$client->receive();
|
|
|
|
|
|
|
|
|
|
// Subscribe (public channel)
|
|
|
|
|
$client->send(json_encode([
|
|
|
|
|
'event' => 'pusher:subscribe',
|
|
|
|
|
'data' => ['channel' => 'websocket'],
|
|
|
|
|
]));
|
|
|
|
|
|
|
|
|
|
// (Optionally read subscription_succeeded)
|
|
|
|
|
$client->receive();
|
|
|
|
|
|
|
|
|
|
// Send event to be processed by Handler
|
|
|
|
|
$client->send(json_encode([
|
|
|
|
|
'event' => $event,
|
|
|
|
|
'channel' => $channel ?? 'websocket',
|
|
|
|
|
'data' => $data,
|
|
|
|
|
]));
|
|
|
|
|
|
|
|
|
|
// Read any response your controller might send (optional)
|
|
|
|
|
$response = $client->receive();
|
|
|
|
|
|
|
|
|
|
$client->close();
|
|
|
|
|
|
|
|
|
|
return json_decode($response);
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
\Log::warning('[WebsocketService] sendViaWebSocket failed: ' . $e->getMessage());
|
|
|
|
|
return (object)['success' => false, 'error' => $e->getMessage()];
|
|
|
|
|
}
|
2025-05-08 08:54:11 +00:00
|
|
|
}
|
2025-09-14 13:00:27 +00:00
|
|
|
|
2025-09-15 08:23:07 +00:00
|
|
|
public static function resetAllTracking()
|
|
|
|
|
{
|
2025-09-15 12:34:50 +00:00
|
|
|
$previousCache = config('cache.default');
|
2025-09-15 08:23:07 +00:00
|
|
|
config(['cache.default' => 'file']);
|
|
|
|
|
cache()->forget('ws_active_channels');
|
|
|
|
|
cache()->forget('ws_socket_auth');
|
|
|
|
|
cache()->forget('ws_socket_auth_users');
|
2025-09-15 08:25:38 +00:00
|
|
|
cache()->forget('ws_socket_authed_users');
|
2025-09-15 08:23:07 +00:00
|
|
|
cache()->forget('ws_channel_connections');
|
|
|
|
|
cache()->forget('ws_connection');
|
2025-09-15 12:34:50 +00:00
|
|
|
config(['cache.default' => $previousCache]);
|
2025-09-15 08:23:07 +00:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-15 08:25:38 +00:00
|
|
|
|
2025-09-15 08:23:07 +00:00
|
|
|
public static function getAuth(string $socketId)
|
2025-09-14 13:00:27 +00:00
|
|
|
{
|
2025-09-15 12:34:50 +00:00
|
|
|
$previousCache = config('cache.default');
|
2025-09-14 13:00:27 +00:00
|
|
|
config(['cache.default' => 'file']);
|
2025-09-15 12:34:50 +00:00
|
|
|
$r = cache()->get('ws_socket_auth_' . str()->slug($socketId));
|
|
|
|
|
config(['cache.default' => $previousCache]);
|
|
|
|
|
return $r;
|
2025-09-14 13:00:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 14:38:57 +00:00
|
|
|
public static function getChannelConnections(string $channelName): array
|
2025-09-14 13:00:27 +00:00
|
|
|
{
|
2025-09-15 12:34:50 +00:00
|
|
|
$previousCache = config('cache.default');
|
2025-09-14 13:00:27 +00:00
|
|
|
config(['cache.default' => 'file']);
|
2026-02-03 14:38:57 +00:00
|
|
|
$r = cache()->get('ws_channel_connections_' . $channelName) ?? [];
|
2025-09-15 12:34:50 +00:00
|
|
|
config(['cache.default' => $previousCache]);
|
|
|
|
|
return $r;
|
2025-09-14 13:00:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 14:38:57 +00:00
|
|
|
public static function getActiveChannels(): array
|
2025-09-14 13:00:27 +00:00
|
|
|
{
|
2025-09-15 12:34:50 +00:00
|
|
|
$previousCache = config('cache.default');
|
2025-09-14 13:00:27 +00:00
|
|
|
config(['cache.default' => 'file']);
|
2026-02-03 14:38:57 +00:00
|
|
|
$r = cache()->get('ws_active_channels') ?? [];
|
2025-09-15 12:34:50 +00:00
|
|
|
config(['cache.default' => $previousCache]);
|
|
|
|
|
return $r;
|
2025-09-14 13:00:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function getConnection(string $socketId)
|
|
|
|
|
{
|
2025-09-15 12:34:50 +00:00
|
|
|
$previousCache = config('cache.default');
|
2025-09-14 13:00:27 +00:00
|
|
|
config(['cache.default' => 'file']);
|
2025-09-15 12:34:50 +00:00
|
|
|
$r = cache()->get('ws_connection_' . str()->slug($socketId));
|
|
|
|
|
config(['cache.default' => $previousCache]);
|
|
|
|
|
return $r;
|
2025-09-14 13:00:27 +00:00
|
|
|
}
|
2025-09-15 08:20:13 +00:00
|
|
|
|
2026-02-03 14:38:57 +00:00
|
|
|
public static function getAuthedUsers(): array
|
2025-09-15 08:20:13 +00:00
|
|
|
{
|
2025-09-15 12:34:50 +00:00
|
|
|
$previousCache = config('cache.default');
|
2025-09-15 08:20:13 +00:00
|
|
|
config(['cache.default' => 'file']);
|
2025-09-15 12:34:50 +00:00
|
|
|
$r = cache()->get('ws_socket_authed_users') ?? [];
|
|
|
|
|
config(['cache.default' => $previousCache]);
|
|
|
|
|
return $r;
|
2025-09-15 08:20:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function isUserConnected($userId)
|
|
|
|
|
{
|
2025-09-15 10:40:25 +00:00
|
|
|
return in_array($userId, array_values(static::getAuthedUsers()));
|
2025-09-15 08:20:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function getUserSocketIds($userId)
|
|
|
|
|
{
|
|
|
|
|
$socket_ids = [];
|
|
|
|
|
|
2025-09-15 10:40:25 +00:00
|
|
|
foreach (static::getAuthedUsers() as $socket_id => $u_id) {
|
2025-09-15 08:20:13 +00:00
|
|
|
if ($u_id == $userId) {
|
|
|
|
|
$socket_ids[] = $socket_id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $socket_ids;
|
|
|
|
|
}
|
2025-09-15 10:40:25 +00:00
|
|
|
|
|
|
|
|
public static function setUserAuthed($socketId, $user)
|
|
|
|
|
{
|
|
|
|
|
$authed_users = static::getAuthedUsers();
|
|
|
|
|
$authed_users[$socketId] = $user->id;
|
2025-09-15 12:34:50 +00:00
|
|
|
|
|
|
|
|
$previousCache = config('cache.default');
|
|
|
|
|
config(['cache.default' => 'file']);
|
2025-09-15 10:40:25 +00:00
|
|
|
cache()->forever('ws_socket_authed_users', $authed_users);
|
|
|
|
|
cache()->forever('ws_socket_auth_' . str()->slug($socketId), $user);
|
2025-09-15 12:34:50 +00:00
|
|
|
config(['cache.default' => $previousCache]);
|
2025-09-15 10:40:25 +00:00
|
|
|
|
|
|
|
|
return static::getAuthedUsers();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function clearUserAuthed($socketId)
|
|
|
|
|
{
|
|
|
|
|
$authed_users = static::getAuthedUsers();
|
|
|
|
|
unset($authed_users[$socketId]);
|
2025-09-15 12:34:50 +00:00
|
|
|
|
|
|
|
|
$previousCache = config('cache.default');
|
|
|
|
|
config(['cache.default' => 'file']);
|
2025-09-15 10:40:25 +00:00
|
|
|
cache()->forever('ws_socket_authed_users', $authed_users);
|
|
|
|
|
cache()->forget('ws_socket_auth_' . str()->slug($socketId));
|
2025-09-15 12:34:50 +00:00
|
|
|
config(['cache.default' => $previousCache]);
|
2025-09-15 10:40:25 +00:00
|
|
|
|
|
|
|
|
return static::getAuthedUsers();
|
|
|
|
|
}
|
2025-05-08 08:54:11 +00:00
|
|
|
}
|