Issues (1358)

modules/WebSockets/Server.php (17 issues)

1
<?php
2
/**
3
 * @package  WebSockets
4
 * @category modules
5
 * @author   Nazar Mokrynskyi <[email protected]>
6
 * @license  0BSD
7
 */
8
namespace cs\modules\WebSockets;
9
use
10
	Ratchet\Client\Connector as Client_connector,
0 ignored issues
show
The type Ratchet\Client\Connector was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
	Ratchet\Client\WebSocket as Client_websocket,
0 ignored issues
show
The type Ratchet\Client\WebSocket was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
	Ratchet\ConnectionInterface,
0 ignored issues
show
The type Ratchet\ConnectionInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
	Ratchet\Http\HttpServer,
0 ignored issues
show
The type Ratchet\Http\HttpServer was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
	Ratchet\MessageComponentInterface,
0 ignored issues
show
The type Ratchet\MessageComponentInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
	Ratchet\Server\IoServer,
0 ignored issues
show
The type Ratchet\Server\IoServer was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
	Ratchet\WebSocket\WsServer,
0 ignored issues
show
The type Ratchet\WebSocket\WsServer was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
	React\Dns\Resolver\Factory as Dns_factory,
0 ignored issues
show
The type React\Dns\Resolver\Factory was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
	React\EventLoop\Factory as Loop_factory,
0 ignored issues
show
The type React\EventLoop\Factory was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
	cs\Config,
20
	cs\Event,
21
	cs\Request,
22
	cs\Session,
23
	cs\Singleton,
24
	cs\User,
25
	Exception,
26
	SplObjectStorage;
27
28
/**
29
 * @method static $this instance($check = false)
30
 */
31
class Server implements MessageComponentInterface {
32
	use
33
		Singleton;
34
	/**
35
	 * Message will be delivered to everyone
36
	 */
37
	const SEND_TO_ALL = 1;
38
	/**
39
	 * Message will be delivered to registered users only
40
	 */
41
	const SEND_TO_REGISTERED_USERS = 2;
42
	/**
43
	 * Message will be delivered to users, specified in target (might be array of users)
44
	 */
45
	const SEND_TO_SPECIFIC_USERS = 3;
46
	/**
47
	 * Message will be delivered to users from group, specified in target (might be array of groups)
48
	 */
49
	const SEND_TO_USERS_GROUP = 4;
50
	/**
51
	 * Message will be delivered to users whose connection objects have property with certain value, target should be an array with format [property, value]
52
	 */
53
	const SEND_TO_FILTER = 5;
54
	/**
55
	 * Each object additionally will have properties `user_id`, `session_id`, `session_expire` and `user_groups` with user id and ids of user groups
56
	 * correspondingly
57
	 *
58
	 * @var ConnectionInterface[]|SplObjectStorage
59
	 */
60
	protected $clients;
61
	/**
62
	 * @var ConnectionInterface[]|SplObjectStorage
63
	 */
64
	protected $servers;
65
	/**
66
	 * Connection to master server
67
	 *
68
	 * @var Client_websocket
69
	 */
70
	protected $connection_to_master;
71
	/**
72
	 * @var Pool
73
	 */
74
	protected $pool;
75
	/**
76
	 * Address to WebSockets server in format wss://server/WebSockets or ws://server/WebSockets, so that one WebSockets server can reach another (in case of
77
	 * several servers)
78
	 *
79
	 * @var string
80
	 */
81
	protected $address;
82
	/**
83
	 * @var IoServer
84
	 */
85
	protected $io_server;
86
	/**
87
	 * @var \React\EventLoop\LoopInterface
0 ignored issues
show
The type React\EventLoop\LoopInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
88
	 */
89
	protected $loop;
90
	/**
91
	 * @var Client_connector
92
	 */
93
	protected $client_connector;
94
	/**
95
	 * @var int
96
	 */
97
	protected $listen_port;
98
	/**
99
	 * @var string
100
	 */
101
	protected $listen_locally;
102
	/**
103
	 * @var string
104
	 */
105
	protected $dns_server;
106
	/**
107
	 * @var string
108
	 */
109
	protected $security_key;
110
	/**
111
	 * @var bool
112
	 */
113
	protected $remember_session_ip;
114
	protected function construct () {
115
		$Config                    = Config::instance();
116
		$Request                   = Request::instance();
117
		$module_data               = $Config->module('WebSockets');
118
		$this->listen_port         = $module_data->listen_port;
0 ignored issues
show
Bug Best Practice introduced by
The property listen_port does not exist on cs\Config\Module_Properties. Since you implemented __get, consider adding a @property annotation.
Loading history...
Documentation Bug introduced by
It seems like $module_data->listen_port can also be of type false. However, the property $listen_port is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
119
		$this->listen_locally      = $module_data->listen_locally ? '127.0.0.1' : '0.0.0.0';
0 ignored issues
show
Bug Best Practice introduced by
The property listen_locally does not exist on cs\Config\Module_Properties. Since you implemented __get, consider adding a @property annotation.
Loading history...
120
		$this->dns_server          = $module_data->dns_server ?: '127.0.0.1';
0 ignored issues
show
Bug Best Practice introduced by
The property dns_server does not exist on cs\Config\Module_Properties. Since you implemented __get, consider adding a @property annotation.
Loading history...
121
		$this->security_key        = $module_data->security_key;
0 ignored issues
show
Documentation Bug introduced by
It seems like $module_data->security_key can also be of type false. However, the property $security_key is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
Bug Best Practice introduced by
The property security_key does not exist on cs\Config\Module_Properties. Since you implemented __get, consider adding a @property annotation.
Loading history...
122
		$this->remember_session_ip = $Config->core['remember_user_ip'];
123
		$this->pool                = Pool::instance();
124
		$this->clients             = new SplObjectStorage;
125
		$this->servers             = new SplObjectStorage;
126
		$this->address             = ($Request->secure ? 'wss' : 'ws')."://$Request->host/WebSockets";
127
	}
128
	/**
129
	 * Run WebSockets server
130
	 *
131
	 * @param null|string $address
132
	 */
133
	public function run ($address = null) {
134
		$this->address = $address ?: $this->address;
135
		@ini_set('error_log', LOGS.'/WebSockets-server.log');
136
		$ws_server              = new WsServer($this);
137
		$this->io_server        = IoServer::factory(
138
			new HttpServer(
139
				new Connection_properties_injector($ws_server)
140
			),
141
			$this->listen_port,
142
			$this->listen_locally
143
		);
144
		$this->loop             = $this->io_server->loop;
145
		$this->client_connector = new Client_connector(
146
			$this->loop,
147
			(new Dns_factory)->create(
148
				$this->dns_server,
149
				$this->loop
150
			)
151
		);
152
		$this->connect_to_master();
153
		// Since we may work with a lot of different users - disable this cache in order to not run out of memory
154
		User::instance()->disable_memory_cache();
155
		$this->loop->run();
156
	}
157
	/**
158
	 * @param ConnectionInterface $connection
159
	 */
160
	public function onOpen (ConnectionInterface $connection) {
161
		$this->clients->attach($connection);
162
	}
163
	/**
164
	 * @param ConnectionInterface $connection
165
	 * @param string              $message
166
	 */
167
	public function onMessage (ConnectionInterface $connection, $message) {
168
		$this->on_message_internal($connection, $message);
169
	}
170
	/**
171
	 * TODO: Probably split this into 2 methods
172
	 *
173
	 * @param Client_websocket|ConnectionInterface $connection
174
	 * @param string                               $message
175
	 */
176
	protected function on_message_internal ($connection, $message) {
177
		$from_master = $connection === $this->connection_to_master;
178
		if (!$this->parse_message($message, $action, $details, $send_to, $target)) {
179
			if (!$from_master) {
180
				$connection->close();
181
			}
182
			return;
183
		}
184
		switch ($action) {
185
			/**
186
			 * Connection to master server as server (by default all connections considered as clients)
187
			 */
188
			case "Server/connect:$this->security_key":
189
				/**
190
				 * Under certain circumstances it may happen so that one server become available through multiple addresses,
191
				 * in this case we need to remove one of them from list of pools
192
				 */
193
				if ($details['from_slave'] === $this->address) {
194
					$this->pool->del($details['to_master']);
195
					$connection->close();
196
				} else {
197
					$this->clients->detach($connection);
198
					$this->servers->attach($connection);
199
				}
200
				return;
201
			/**
202
			 * Internal connection from application
203
			 */
204
			case "Application/Internal:$this->security_key":
205
				if ($this->parse_message($details, $action_, $details_, $send_to_, $target_)) {
206
					$connection->close();
207
					$this->send_to_clients($action_, $details_, $send_to_, $target_);
208
				}
209
				return;
210
			case 'Client/authentication':
211
				$Session = Session::instance();
212
				/** @noinspection PhpUndefinedFieldInspection */
213
				$session = $Session->get($connection->session_id);
214
				/** @noinspection PhpUndefinedFieldInspection */
215
				if (
216
					!$session ||
0 ignored issues
show
The condition $session is always false.
Loading history...
217
					!$Session->is_session_owner($session['id'], $connection->user_agent, $connection->remote_addr, $connection->ip)
218
				) {
219
					$connection->send(
220
						_json_encode(['Client/authentication:error', $this->compose_error(403)])
221
					);
222
					$connection->close();
223
					return;
224
				}
225
				$connection->user_id        = $session['user'];
226
				$connection->session_id     = $session['id'];
227
				$connection->session_expire = $session['expire'];
228
				$connection->groups         = User::instance()->get_groups($session['user']);
229
				$connection->send(
230
					_json_encode(['Client/authentication', 'ok'])
231
				);
232
		}
233
		if ($from_master) {
234
			$this->send_to_clients_internal($action, $details, $send_to, $target);
235
		} elseif ($this->servers->contains($connection)) {
236
			$this->broadcast_message_to_servers($message, $connection);
237
			if (!$send_to) {
238
				return;
239
			}
240
			$this->send_to_clients_internal($action, $details, $send_to, $target);
241
		} elseif (property_exists($connection, 'user_id')) {
242
			/** @noinspection PhpUndefinedFieldInspection */
243
			Event::instance()->fire(
244
				'WebSockets/message',
245
				[
246
					'action'     => $action,
247
					'details'    => $details,
248
					'language'   => $connection->language,
249
					'user'       => $connection->user_id,
250
					'session'    => $connection->session_id,
251
					'connection' => $connection
252
				]
253
			);
254
		}
255
	}
256
	/**
257
	 * @param string    $message
258
	 * @param string    $action
259
	 * @param mixed     $details
260
	 * @param int|int[] $send_to
261
	 * @param int       $target
262
	 *
263
	 * @return bool
264
	 */
265
	protected function parse_message ($message, &$action, &$details, &$send_to, &$target) {
266
		$decoded_message = _json_decode($message);
267
		if (
268
			!is_array($decoded_message) ||
269
			!array_key_exists(0, $decoded_message) ||
270
			!array_key_exists(1, $decoded_message)
271
		) {
272
			return false;
273
		}
274
		list($action, $details) = $decoded_message;
275
		$send_to = isset($decoded_message[2]) ? $decoded_message[2] : 0;
276
		$target  = isset($decoded_message[3]) ? $decoded_message[3] : 0;
277
		return true;
278
	}
279
	/**
280
	 * @param string                   $message
281
	 * @param ConnectionInterface|null $skip_server
282
	 */
283
	protected function broadcast_message_to_servers ($message, $skip_server = null) {
284
		foreach ($this->servers as $server) {
285
			if ($server === $skip_server) {
286
				continue;
287
			}
288
			$server->send($message);
289
		}
290
	}
291
	/**
292
	 * Compose error
293
	 *
294
	 * @param int         $error_code    HTTP status code
295
	 * @param null|string $error_message String representation of status code
296
	 *
297
	 * @return array Array to be passed as details to `::send_to_clients()`
298
	 */
299
	public function compose_error ($error_code, $error_message = null) {
300
		$error_message = $error_message ?: status_code_string($error_code);
301
		return [$error_code, $error_message];
302
	}
303
	/**
304
	 * Send request to client
305
	 *
306
	 * @param string          $action
307
	 * @param mixed           $details
308
	 * @param int             $send_to Constants `self::SEND_TO*` should be used here
309
	 * @param false|int|int[] $target  Id or array of ids in case of response to one or several users or groups
310
	 */
311
	public function send_to_clients ($action, $details, $send_to, $target = false) {
312
		$message = _json_encode([$action, $details, $send_to, $target]);
313
		/**
314
		 * If server running in current process
315
		 */
316
		if ($this->io_server) {
317
			if ($this->connection_to_master) {
318
				$this->connection_to_master->send($message);
319
			} else {
320
				$this->broadcast_message_to_servers($message);
321
			}
322
			$this->send_to_clients_internal($action, $details, $send_to, $target);
323
			return;
324
		}
325
		$servers = $this->pool->get_all();
326
		if ($servers) {
327
			shuffle($servers);
328
			$loop      = Loop_factory::create();
329
			$connector = new Client_connector($loop);
330
			$connector($servers[0])->then(
331
				function (Client_websocket $connection) use ($message) {
332
					$connection->send(
333
						_json_encode(["Application/Internal:$this->security_key", $message])
334
					);
335
					// Connection will be closed by server itself, no need to stop loop here
336
				},
337
				function () use ($loop) {
338
					$loop->stop();
339
				}
340
			);
341
			$loop->run();
342
		}
343
	}
344
	/**
345
	 * Send request to client
346
	 *
347
	 * @param string                  $action
348
	 * @param mixed                   $details
349
	 * @param int                     $send_to Constants `self::SEND_TO_*` should be used here
350
	 * @param false|int|int[]|mixed[] $target  Id or array of ids in case of response to one or several users or groups, [property, value] for filter
351
	 */
352
	protected function send_to_clients_internal ($action, $details, $send_to, $target = false) {
353
		$message = _json_encode([$action, $details]);
354
		/**
355
		 * Special system actions
356
		 */
357
		switch ($action) {
358
			case 'Server/close_by_session':
359
				foreach ($this->clients as $client) {
360
					if ($client->session_id == $details) {
361
						$client->send(_json_encode('Server/close'));
362
						$client->close();
363
					}
364
				}
365
				return;
366
			case 'Server/close_by_user':
367
				foreach ($this->clients as $client) {
368
					if ($client->user_id == $details) {
369
						$client->send(_json_encode('Server/close'));
370
						$client->close();
371
					}
372
				}
373
				return;
374
		}
375
		switch ($send_to) {
376
			case self::SEND_TO_ALL:
377
				foreach ($this->clients as $client) {
378
					$client->send($message);
379
				}
380
				break;
381
			case self::SEND_TO_REGISTERED_USERS:
382
				foreach ($this->clients as $client) {
383
					if (isset($client->user_id)) {
384
						$this->send_to_client_if_not_expire($client, $message);
385
					}
386
				}
387
				break;
388
			case self::SEND_TO_SPECIFIC_USERS:
389
				$target = (array)$target;
390
				foreach ($this->clients as $client) {
391
					if (isset($client->user_id) && in_array($client->user_id, $target)) {
392
						$this->send_to_client_if_not_expire($client, $message);
393
					}
394
				}
395
				break;
396
			case self::SEND_TO_USERS_GROUP:
397
				$target = (array)$target;
398
				foreach ($this->clients as $client) {
399
					if (isset($client->user_groups) && array_intersect($client->user_groups, $target)) {
400
						$this->send_to_client_if_not_expire($client, $message);
401
					}
402
				}
403
				break;
404
			case self::SEND_TO_FILTER:
405
				list($property, $value) = $target;
406
				foreach ($this->clients as $client) {
407
					if (isset($client->$property) && $client->$property === $value) {
408
						$this->send_to_client_if_not_expire($client, $message);
409
					}
410
				}
411
				break;
412
		}
413
	}
414
	/**
415
	 * If session not expire - will send message, otherwise will disconnect
416
	 *
417
	 * @param ConnectionInterface $client
418
	 * @param string              $message
419
	 */
420
	protected function send_to_client_if_not_expire ($client, $message) {
421
		/** @noinspection PhpUndefinedFieldInspection */
422
		if ($client->session_expire >= time()) {
423
			$client->send($message);
424
		} else {
425
			$client->close();
426
		}
427
	}
428
	/**
429
	 * Close all client connections by specified session id
430
	 *
431
	 * @param string $session_id
432
	 */
433
	public function close_by_session ($session_id) {
434
		$this->send_to_clients('Server/close_by_session', $session_id, 0);
435
	}
436
	/**
437
	 * Close all client connections by specified user id
438
	 *
439
	 * @param string $user_id
440
	 */
441
	public function close_by_user ($user_id) {
442
		$this->send_to_clients('Server/close_by_user', $user_id, 0);
443
	}
444
	/**
445
	 * Connect to master server
446
	 *
447
	 * Two trials, if server do not respond twice - it will be removed from servers pool, and next server will become master
448
	 */
449
	protected function connect_to_master () {
450
		static $last_trial = '';
451
		// Add server to connections pool and connect to master if any
452
		$this->pool->add($this->address);
453
		$master = $this->pool->get_master();
454
		if ($master && $master != $this->address) {
455
			call_user_func($this->client_connector, $master)->then(
456
				function (Client_websocket $connection) use (&$last_trial, $master) {
457
					$last_trial                 = '';
458
					$this->connection_to_master = $connection;
459
					/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
460
					$connection
461
						->on(
462
							'message',
463
							function ($message) use ($connection) {
464
								$this->on_message_internal($connection, $message);
465
							}
466
						)
467
						->on(
468
							'error',
469
							function () use ($connection) {
470
								$connection->close();
471
							}
472
						)
473
						->on(
474
							'close',
475
							function () {
476
								$this->connection_to_master = null;
477
								$this->loop->addTimer(
478
									1,
479
									function () {
480
										$this->connect_to_master();
481
									}
482
								);
483
							}
484
						);
485
					/**
486
					 * Tell master that we are server also, not regular client
487
					 */
488
					$connection->send(
489
						_json_encode(
490
							[
491
								"Server/connect:$this->security_key",
492
								[
493
									'to_master'  => $master,
494
									'from_slave' => $this->address
495
								]
496
							]
497
						)
498
					);
499
				},
500
				function () use (&$last_trial, $master) {
501
					if ($last_trial == $master) {
502
						$this->pool->del($master);
503
					} else {
504
						$last_trial = $master;
505
					}
506
					$this->loop->addTimer(
507
						1,
508
						function () {
509
							$this->connect_to_master();
510
						}
511
					);
512
					$this->connect_to_master();
513
				}
514
			);
515
		} else {
516
			$last_trial = '';
517
			/**
518
			 * Sometimes other servers may loose connection with master server, so new master will be selected and we need to handle this nicely
519
			 */
520
			$this->loop->addTimer(
521
				30,
522
				function () {
523
					$this->connect_to_master();
524
				}
525
			);
526
		}
527
	}
528
	/**
529
	 * Get event loop instance
530
	 *
531
	 * @return \React\EventLoop\LoopInterface
532
	 */
533
	public function get_loop () {
534
		return $this->loop;
535
	}
536
	/**
537
	 * @param ConnectionInterface $connection
538
	 */
539
	public function onClose (ConnectionInterface $connection) {
540
		/**
541
		 * Generate pseudo-event when client is disconnected
542
		 */
543
		if (isset($connection->user_id) && $this->clients->contains($connection)) {
544
			/** @noinspection PhpUndefinedFieldInspection */
545
			Event::instance()->fire(
546
				'WebSockets/message',
547
				[
548
					'action'     => 'Client/disconnection',
549
					'details'    => null,
550
					'language'   => $connection->language,
551
					'user'       => $connection->user_id,
552
					'session'    => $connection->session_id,
553
					'connection' => $connection
554
				]
555
			);
556
		}
557
		// The connection is closed, remove it, as we can no longer send it messages
558
		$this->clients->detach($connection);
559
		$this->servers->detach($connection);
560
	}
561
	/**
562
	 * @param ConnectionInterface $connection
563
	 * @param Exception           $e
564
	 */
565
	public function onError (ConnectionInterface $connection, Exception $e) {
566
		$connection->close();
567
	}
568
	public function __destruct () {
569
		$this->pool->del($this->address);
570
	}
571
}
572