5
5
use Ratchet \ConnectionInterface ;
6
6
use stdClass ;
7
7
8
+ /**
9
+ * @link https://pusher.com/docs/pusher_protocol#presence-channel-events
10
+ */
8
11
class PresenceChannel extends Channel
9
12
{
13
+ /**
14
+ * List of users in the channel keyed by their user ID with their info as value.
15
+ *
16
+ * @var array<string, array>
17
+ */
10
18
protected $ users = [];
11
19
12
- public function getUsers (): array
13
- {
14
- return $ this ->users ;
15
- }
16
-
17
- /*
18
- * @link https://pusher.com/docs/pusher_protocol#presence-channel-events
20
+ /**
21
+ * List of sockets keyed by their ID with the value pointing to a user ID.
22
+ *
23
+ * @var array<string, string>
19
24
*/
25
+ protected $ sockets = [];
26
+
20
27
public function subscribe (ConnectionInterface $ connection , stdClass $ payload )
21
28
{
22
29
$ this ->verifySignature ($ connection , $ payload );
23
30
24
31
$ this ->saveConnection ($ connection );
25
32
26
- $ channelData = json_decode ($ payload ->channel_data );
27
- $ this ->users [$ connection ->socketId ] = $ channelData ;
33
+ $ channelData = json_decode ($ payload ->channel_data , true );
34
+
35
+ // The ID of the user connecting
36
+ $ userId = (string ) $ channelData ['user_id ' ];
37
+
38
+ // Check if the user was already connected to the channel before storing the connection in the state
39
+ $ userFirstConnection = ! isset ($ this ->users [$ userId ]);
40
+
41
+ // Add or replace the user info in the state
42
+ $ this ->users [$ userId ] = $ channelData ['user_info ' ] ?? [];
43
+
44
+ // Add the socket ID to user ID map in the state
45
+ $ this ->sockets [$ connection ->socketId ] = $ userId ;
28
46
29
47
// Send the success event
30
48
$ connection ->send (json_encode ([
@@ -33,72 +51,74 @@ public function subscribe(ConnectionInterface $connection, stdClass $payload)
33
51
'data ' => json_encode ($ this ->getChannelData ()),
34
52
]));
35
53
36
- $ this ->broadcastToOthers ($ connection , [
37
- 'event ' => 'pusher_internal:member_added ' ,
38
- 'channel ' => $ this ->channelName ,
39
- 'data ' => json_encode ($ channelData ),
40
- ]);
54
+ // The `pusher_internal:member_added` event is triggered when a user joins a channel.
55
+ // It's quite possible that a user can have multiple connections to the same channel
56
+ // (for example by having multiple browser tabs open)
57
+ // and in this case the events will only be triggered when the first tab is opened.
58
+ if ($ userFirstConnection ) {
59
+ $ this ->broadcastToOthers ($ connection , [
60
+ 'event ' => 'pusher_internal:member_added ' ,
61
+ 'channel ' => $ this ->channelName ,
62
+ 'data ' => json_encode ($ channelData ),
63
+ ]);
64
+ }
41
65
}
42
66
43
67
public function unsubscribe (ConnectionInterface $ connection )
44
68
{
45
69
parent ::unsubscribe ($ connection );
46
70
47
- if (! isset ($ this ->users [$ connection ->socketId ])) {
71
+ if (! isset ($ this ->sockets [$ connection ->socketId ])) {
48
72
return ;
49
73
}
50
74
51
- $ this ->broadcastToOthers ($ connection , [
52
- 'event ' => 'pusher_internal:member_removed ' ,
53
- 'channel ' => $ this ->channelName ,
54
- 'data ' => json_encode ([
55
- 'user_id ' => $ this ->users [$ connection ->socketId ]->user_id ,
56
- ]),
57
- ]);
58
-
59
- unset($ this ->users [$ connection ->socketId ]);
75
+ // Find the user ID belonging to this socket
76
+ $ userId = $ this ->sockets [$ connection ->socketId ];
77
+
78
+ // Remove the socket from the state
79
+ unset($ this ->sockets [$ connection ->socketId ]);
80
+
81
+ // Test if the user still has open sockets to this channel
82
+ $ userHasOpenConnections = (array_flip ($ this ->sockets )[$ userId ] ?? null ) !== null ;
83
+
84
+ // The `pusher_internal:member_removed` is triggered when a user leaves a channel.
85
+ // It's quite possible that a user can have multiple connections to the same channel
86
+ // (for example by having multiple browser tabs open)
87
+ // and in this case the events will only be triggered when the last one is closed.
88
+ if (! $ userHasOpenConnections ) {
89
+ $ this ->broadcastToOthers ($ connection , [
90
+ 'event ' => 'pusher_internal:member_removed ' ,
91
+ 'channel ' => $ this ->channelName ,
92
+ 'data ' => json_encode ([
93
+ 'user_id ' => $ userId ,
94
+ ]),
95
+ ]);
96
+
97
+ // Remove the user info from the state
98
+ unset($ this ->users [$ userId ]);
99
+ }
60
100
}
61
101
62
102
protected function getChannelData (): array
63
103
{
64
104
return [
65
105
'presence ' => [
66
- 'ids ' => $ userIds = $ this ->getUserIds ( ),
67
- 'hash ' => $ this ->getHash () ,
68
- 'count ' => count ($ userIds ),
106
+ 'ids ' => array_keys ( $ this ->users ),
107
+ 'hash ' => $ this ->users ,
108
+ 'count ' => count ($ this -> users ),
69
109
],
70
110
];
71
111
}
72
112
73
- public function toArray (): array
74
- {
75
- return array_merge (parent ::toArray (), [
76
- 'user_count ' => count ($ this ->getUserIds ()),
77
- ]);
78
- }
79
-
80
- protected function getUserIds (): array
113
+ public function getUsers (): array
81
114
{
82
- $ userIds = array_map (function ($ channelData ) {
83
- return (string ) $ channelData ->user_id ;
84
- }, $ this ->users );
85
-
86
- return array_values (array_unique ($ userIds ));
115
+ return $ this ->users ;
87
116
}
88
117
89
- /**
90
- * Compute the hash for the presence channel integrity.
91
- *
92
- * @return array
93
- */
94
- protected function getHash (): array
118
+ public function toArray (): array
95
119
{
96
- $ hash = [];
97
-
98
- foreach ($ this ->users as $ socketId => $ channelData ) {
99
- $ hash [$ channelData ->user_id ] = $ channelData ->user_info ?? [];
100
- }
101
-
102
- return $ hash ;
120
+ return array_merge (parent ::toArray (), [
121
+ 'user_count ' => count ($ this ->users ),
122
+ ]);
103
123
}
104
124
}
0 commit comments