55use Ratchet \ConnectionInterface ;
66use stdClass ;
77
8+ /**
9+ * @link https://pusher.com/docs/pusher_protocol#presence-channel-events
10+ */
811class PresenceChannel extends Channel
912{
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+ */
1018 protected $ users = [];
1119
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>
1924 */
25+ protected $ sockets = [];
26+
2027 public function subscribe (ConnectionInterface $ connection , stdClass $ payload )
2128 {
2229 $ this ->verifySignature ($ connection , $ payload );
2330
2431 $ this ->saveConnection ($ connection );
2532
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 ;
2846
2947 // Send the success event
3048 $ connection ->send (json_encode ([
@@ -33,72 +51,74 @@ public function subscribe(ConnectionInterface $connection, stdClass $payload)
3351 'data ' => json_encode ($ this ->getChannelData ()),
3452 ]));
3553
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+ }
4165 }
4266
4367 public function unsubscribe (ConnectionInterface $ connection )
4468 {
4569 parent ::unsubscribe ($ connection );
4670
47- if (! isset ($ this ->users [$ connection ->socketId ])) {
71+ if (! isset ($ this ->sockets [$ connection ->socketId ])) {
4872 return ;
4973 }
5074
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+ }
60100 }
61101
62102 protected function getChannelData (): array
63103 {
64104 return [
65105 '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 ),
69109 ],
70110 ];
71111 }
72112
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
81114 {
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 ;
87116 }
88117
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
95119 {
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+ ]);
103123 }
104124}
0 commit comments