Namespaces and Rooms
Namespaces and rooms provide powerful organization and grouping capabilities for WebSocket clients. They allow you to create logical separations and targeted broadcasting, essential for building scalable real-time applications.
Understanding Namespaces and Rooms
Namespaces
Namespaces provide the top-level organization for your application. Think of them as different "areas" or "sections" of your application:
/
- Default namespace (all clients start here)/chat
- Chat application namespace/game
- Game namespace/admin
- Admin panel namespace
Rooms
Rooms are subdivisions within namespaces. They're perfect for organizing clients into smaller groups:
general
- General chat roomroom_123
- Specific chat roomgame_456
- Specific game sessionuser_789
- Private user space
Hierarchy
Namespace: /chat
├── Room: general
│ ├── Client 1
│ ├── Client 2
│ └── Client 3
├── Room: room_123
│ ├── Client 4
│ └── Client 5
└── Room: private_456
└── Client 6
Namespace: /game
├── Room: lobby
│ ├── Client 7
│ └── Client 8
└── Room: game_789
├── Client 9
└── Client 10
Working with Namespaces
Joining Namespaces
Clients are automatically placed in the default namespace (/
) when they connect. You can move them to other namespaces:
class ChatController extends SocketController
{
#[OnConnect]
public function onConnect(int $clientId): void
{
// Client is automatically in '/' namespace
// Move to chat namespace
$this->moveClientToNamespace($clientId, '/chat');
$this->emit($clientId, 'namespace.joined', [
'namespace' => '/chat',
'message' => 'Welcome to the chat namespace!'
]);
}
#[SocketOn('namespace.switch')]
public function switchNamespace(int $clientId, array $data): void
{
$targetNamespace = $data['namespace'] ?? '/';
// Validate namespace
$allowedNamespaces = ['/', '/chat', '/game', '/admin'];
if (!in_array($targetNamespace, $allowedNamespaces)) {
$this->emit($clientId, 'error', ['message' => 'Invalid namespace']);
return;
}
// Move to new namespace
$this->moveClientToNamespace($clientId, $targetNamespace);
$this->emit($clientId, 'namespace.switched', [
'namespace' => $targetNamespace
]);
}
}
Broadcasting to Namespaces
Send messages to all clients in a specific namespace:
class NotificationController extends SocketController
{
#[HttpRoute('POST', '/api/notifications/broadcast')]
public function broadcastToNamespace(Request $request): Response
{
$data = $request->all();
$namespace = $data['namespace'] ?? '/';
$message = $data['message'] ?? '';
// Broadcast to all clients in the namespace
$this->broadcastToNamespaceClients('notification', [
'message' => $message,
'timestamp' => time(),
'type' => 'system'
], $namespace);
return Response::json(['success' => true]);
}
#[SocketOn('admin.announcement')]
public function adminAnnouncement(int $clientId, array $data): void
{
$message = $data['message'] ?? '';
$targetNamespace = $data['namespace'] ?? '/';
// Send announcement to specific namespace
$this->broadcastToNamespaceClients('announcement', [
'message' => $message,
'from' => 'admin',
'timestamp' => time()
], $targetNamespace);
// Confirm to admin
$this->emit($clientId, 'announcement.sent', [
'namespace' => $targetNamespace,
'message' => $message
]);
}
}
Working with Rooms
Joining and Leaving Rooms
class ChatController extends SocketController
{
#[OnConnect]
public function onConnect(int $clientId): void
{
// Move to chat namespace
$this->moveClientToNamespace($clientId, '/chat');
// Join default room
$this->joinRoom($clientId, 'general', '/chat');
// Notify room about new user
$this->broadcastToRoomClients('user.joined', [
'clientId' => $clientId,
'message' => "User {$clientId} joined the room"
], 'general', '/chat');
}
#[SocketOn('room.join')]
public function joinChatRoom(int $clientId, array $data): void
{
$room = $data['room'] ?? 'general';
$namespace = '/chat';
// Leave current room(s) first (optional)
$this->leaveAllRooms($clientId, $namespace);
// Join new room
$this->joinRoom($clientId, $room, $namespace);
// Confirm to user
$this->emit($clientId, 'room.joined', [
'room' => $room,
'namespace' => $namespace
]);
// Notify room members
$this->broadcastToRoomClients('user.joined', [
'clientId' => $clientId,
'room' => $room
], $room, $namespace);
}
#[SocketOn('room.leave')]
public function leaveChatRoom(int $clientId, array $data): void
{
$room = $data['room'] ?? 'general';
$namespace = '/chat';
// Leave the room
$this->leaveRoom($clientId, $room, $namespace);
// Notify remaining room members
$this->broadcastToRoomClients('user.left', [
'clientId' => $clientId,
'room' => $room
], $room, $namespace);
// Confirm to user
$this->emit($clientId, 'room.left', [
'room' => $room
]);
}
}
Broadcasting to Rooms
Send messages to all clients in a specific room:
class ChatController extends SocketController
{
#[SocketOn('chat.message')]
public function sendMessage(int $clientId, array $data): void
{
$message = $data['message'] ?? '';
$room = $data['room'] ?? 'general';
$namespace = '/chat';
if (empty($message)) {
$this->emit($clientId, 'error', ['message' => 'Message cannot be empty']);
return;
}
// Broadcast to room
$this->broadcastToRoomClients('chat.message', [
'clientId' => $clientId,
'message' => $message,
'room' => $room,
'timestamp' => time()
], $room, $namespace);
}
#[SocketOn('chat.private')]
public function sendPrivateMessage(int $clientId, array $data): void
{
$targetId = $data['targetId'] ?? null;
$message = $data['message'] ?? '';
if (!$targetId || !$this->isClientConnected($targetId)) {
$this->emit($clientId, 'error', ['message' => 'Target user not found']);
return;
}
// Create private room name
$privateRoom = 'private_' . min($clientId, $targetId) . '_' . max($clientId, $targetId);
// Ensure both users are in the private room
$this->joinRoom($clientId, $privateRoom, '/chat');
$this->joinRoom($targetId, $privateRoom, '/chat');
// Send message to private room
$this->broadcastToRoomClients('chat.private', [
'from' => $clientId,
'message' => $message,
'timestamp' => time()
], $privateRoom, '/chat');
}
}
Advanced Room Management
Dynamic Room Creation
class GameController extends SocketController
{
private array $games = [];
#[SocketOn('game.create')]
public function createGame(int $clientId, array $data): void
{
$gameName = $data['name'] ?? 'Untitled Game';
$maxPlayers = $data['maxPlayers'] ?? 4;
$gameId = uniqid('game_');
// Store game info
$this->games[$gameId] = [
'id' => $gameId,
'name' => $gameName,
'host' => $clientId,
'maxPlayers' => $maxPlayers,
'players' => [$clientId],
'status' => 'waiting',
'created' => time()
];
// Move host to game namespace and room
$this->moveClientToNamespace($clientId, '/game');
$this->joinRoom($clientId, $gameId, '/game');
// Notify host
$this->emit($clientId, 'game.created', [
'gameId' => $gameId,
'game' => $this->games[$gameId]
]);
// Notify lobby about new game
$this->broadcastToRoomClients('game.available', [
'gameId' => $gameId,
'name' => $gameName,
'host' => $clientId,
'players' => 1,
'maxPlayers' => $maxPlayers
], 'lobby', '/game');
}
#[SocketOn('game.join')]
public function joinGame(int $clientId, array $data): void
{
$gameId = $data['gameId'] ?? null;
if (!isset($this->games[$gameId])) {
$this->emit($clientId, 'error', ['message' => 'Game not found']);
return;
}
$game = &$this->games[$gameId];
// Check if game is full
if (count($game['players']) >= $game['maxPlayers']) {
$this->emit($clientId, 'error', ['message' => 'Game is full']);
return;
}
// Add player to game
$game['players'][] = $clientId;
// Move player to game namespace and room
$this->moveClientToNamespace($clientId, '/game');
$this->joinRoom($clientId, $gameId, '/game');
// Notify all players in the game
$this->broadcastToRoomClients('player.joined', [
'playerId' => $clientId,
'playerCount' => count($game['players']),
'maxPlayers' => $game['maxPlayers']
], $gameId, '/game');
// If game is now full, start it
if (count($game['players']) >= $game['maxPlayers']) {
$game['status'] = 'playing';
$this->broadcastToRoomClients('game.started', [
'gameId' => $gameId,
'players' => $game['players']
], $gameId, '/game');
}
}
#[OnDisconnect]
public function onDisconnect(int $clientId): void
{
// Clean up games when host disconnects
foreach ($this->games as $gameId => $game) {
if ($game['host'] === $clientId) {
// Notify players
$this->broadcastToRoomClients('game.ended', [
'reason' => 'Host disconnected'
], $gameId, '/game');
// Remove game
unset($this->games[$gameId]);
} elseif (in_array($clientId, $game['players'])) {
// Remove player from game
$this->games[$gameId]['players'] = array_filter(
$game['players'],
fn($id) => $id !== $clientId
);
// Notify remaining players
$this->broadcastToRoomClients('player.left', [
'playerId' => $clientId,
'playerCount' => count($this->games[$gameId]['players'])
], $gameId, '/game');
}
}
}
}
Room Information and Management
class RoomManagerController extends SocketController
{
#[HttpRoute('GET', '/api/rooms')]
public function listRooms(Request $request): Response
{
$namespace = $request->getQuery('namespace', '/');
$rooms = $this->getServer()->getNamespaceManager()->getRoomsInNamespace($namespace);
return Response::json([
'namespace' => $namespace,
'rooms' => $rooms
]);
}
#[HttpRoute('GET', '/api/rooms/{room}/clients')]
public function getRoomClients(Request $request): Response
{
$room = $request->getParam('room');
$namespace = $request->getQuery('namespace', '/');
$clients = $this->getServer()->getNamespaceManager()->getClientsInRoom($room, $namespace);
return Response::json([
'room' => $room,
'namespace' => $namespace,
'clients' => $clients,
'count' => count($clients)
]);
}
#[SocketOn('room.list')]
public function listAvailableRooms(int $clientId, array $data): void
{
$namespace = $data['namespace'] ?? '/';
$rooms = $this->getServer()->getNamespaceManager()->getRoomsInNamespace($namespace);
$roomList = [];
foreach ($rooms as $room => $clients) {
$roomList[] = [
'name' => $room,
'clientCount' => count($clients),
'clients' => array_values($clients)
];
}
$this->emit($clientId, 'room.list', [
'namespace' => $namespace,
'rooms' => $roomList
]);
}
}
Real-World Examples
Multi-Tenant Chat Application
class MultiTenantChatController extends SocketController
{
#[OnConnect]
public function onConnect(int $clientId): void
{
// Clients start in default namespace
$this->emit($clientId, 'connected', [
'clientId' => $clientId,
'message' => 'Please select a tenant to join'
]);
}
#[SocketOn('tenant.join')]
public function joinTenant(int $clientId, array $data): void
{
$tenantId = $data['tenantId'] ?? null;
$userId = $data['userId'] ?? null;
if (!$tenantId || !$userId) {
$this->emit($clientId, 'error', ['message' => 'Tenant ID and User ID required']);
return;
}
// Create tenant-specific namespace
$namespace = "/tenant_{$tenantId}";
// Join tenant namespace
$this->moveClientToNamespace($clientId, $namespace);
// Join general room in tenant
$this->joinRoom($clientId, 'general', $namespace);
// Store user info
$this->setClientData($clientId, 'userId', $userId);
$this->setClientData($clientId, 'tenantId', $tenantId);
// Notify tenant about new user
$this->broadcastToNamespaceClients('user.joined', [
'userId' => $userId,
'clientId' => $clientId,
'tenantId' => $tenantId
], $namespace);
$this->emit($clientId, 'tenant.joined', [
'tenantId' => $tenantId,
'namespace' => $namespace
]);
}
#[SocketOn('chat.message')]
public function sendMessage(int $clientId, array $data): void
{
$userId = $this->getClientData($clientId, 'userId');
$tenantId = $this->getClientData($clientId, 'tenantId');
if (!$tenantId) {
$this->emit($clientId, 'error', ['message' => 'Not connected to any tenant']);
return;
}
$message = $data['message'] ?? '';
$room = $data['room'] ?? 'general';
$namespace = "/tenant_{$tenantId}";
// Broadcast to tenant room
$this->broadcastToRoomClients('chat.message', [
'userId' => $userId,
'message' => $message,
'room' => $room,
'timestamp' => time()
], $room, $namespace);
}
}
Real-Time Collaboration
class CollaborationController extends SocketController
{
#[SocketOn('document.join')]
public function joinDocument(int $clientId, array $data): void
{
$documentId = $data['documentId'] ?? null;
$userId = $data['userId'] ?? null;
if (!$documentId || !$userId) {
$this->emit($clientId, 'error', ['message' => 'Document ID and User ID required']);
return;
}
// Join collaboration namespace
$namespace = '/collaboration';
$this->moveClientToNamespace($clientId, $namespace);
// Join document-specific room
$room = "doc_{$documentId}";
$this->joinRoom($clientId, $room, $namespace);
// Store user info
$this->setClientData($clientId, 'userId', $userId);
$this->setClientData($clientId, 'documentId', $documentId);
// Notify other collaborators
$this->broadcastToRoomClients('user.joined.document', [
'userId' => $userId,
'documentId' => $documentId,
'clientId' => $clientId
], $room, $namespace);
$this->emit($clientId, 'document.joined', [
'documentId' => $documentId,
'room' => $room
]);
}
#[SocketOn('document.edit')]
public function editDocument(int $clientId, array $data): void
{
$userId = $this->getClientData($clientId, 'userId');
$documentId = $this->getClientData($clientId, 'documentId');
if (!$documentId) {
$this->emit($clientId, 'error', ['message' => 'Not connected to any document']);
return;
}
$namespace = '/collaboration';
$room = "doc_{$documentId}";
// Broadcast edit to all collaborators except sender
$this->broadcastToRoomClients('document.edit', [
'userId' => $userId,
'documentId' => $documentId,
'operation' => $data['operation'] ?? null,
'position' => $data['position'] ?? null,
'content' => $data['content'] ?? null,
'timestamp' => time()
], $room, $namespace);
}
#[SocketOn('cursor.position')]
public function updateCursorPosition(int $clientId, array $data): void
{
$userId = $this->getClientData($clientId, 'userId');
$documentId = $this->getClientData($clientId, 'documentId');
if (!$documentId) {
return;
}
$namespace = '/collaboration';
$room = "doc_{$documentId}";
// Broadcast cursor position to other collaborators
$this->broadcastToRoomClients('cursor.position', [
'userId' => $userId,
'position' => $data['position'] ?? null,
'selection' => $data['selection'] ?? null
], $room, $namespace);
}
}
Best Practices
1. Namespace Organization
// Good - clear separation by feature
'/chat' // Chat functionality
'/game' // Game functionality
'/admin' // Admin panel
'/api' // API connections
// Bad - unclear or too nested
'/app/chat/general' // Too nested
'/stuff' // Unclear purpose
2. Room Naming
// Good - descriptive and consistent
'general' // General chat room
'room_123' // Specific room with ID
'game_456' // Game session
'private_1_2' // Private room between users 1 and 2
// Bad - unclear or inconsistent
'r123' // Unclear abbreviation
'TheAwesomeRoom' // Inconsistent casing
3. Automatic Cleanup
Always clean up when clients disconnect:
#[OnDisconnect]
public function onDisconnect(int $clientId): void
{
// Sockeon automatically removes clients from namespaces and rooms
// But you should handle application-specific cleanup
$this->cleanupClientData($clientId);
$this->notifyClientLeft($clientId);
}
4. Error Handling
Handle invalid namespace/room operations:
#[SocketOn('room.join')]
public function joinRoom(int $clientId, array $data): void
{
$room = $data['room'] ?? null;
if (!$room || !$this->isValidRoomName($room)) {
$this->emit($clientId, 'error', ['message' => 'Invalid room name']);
return;
}
if (!$this->canUserJoinRoom($clientId, $room)) {
$this->emit($clientId, 'error', ['message' => 'Access denied']);
return;
}
$this->joinRoom($clientId, $room);
}
Next Steps
- WebSocket Events - Advanced event handling
- Broadcasting - Targeted message broadcasting
- Examples - See namespaces and rooms in real applications