Completed
Push — master ( b3aa92...a7ea1b )
by Nazar
06:13
created

Server::on_message_internal()   C

Complexity

Conditions 14
Paths 17

Size

Total Lines 76
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 51
nc 17
nop 2
dl 0
loc 76
rs 5.2661
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @package   WebSockets
4
 * @category  modules
5
 * @author    Nazar Mokrynskyi <[email protected]>
6
 * @copyright Copyright (c) 2015-2017, Nazar Mokrynskyi
7
 * @license   MIT License, see license.txt
8
 */
9
namespace cs\modules\WebSockets;
10
use
11
	Ratchet\Client\Connector as Client_connector,
0 ignored issues
show
Bug introduced by
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...
12
	Ratchet\Client\WebSocket as Client_websocket,
0 ignored issues
show
Bug introduced by
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...
13
	Ratchet\ConnectionInterface,
0 ignored issues
show
Bug introduced by
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...
14
	Ratchet\Http\HttpServer,
0 ignored issues
show
Bug introduced by
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...
15
	Ratchet\MessageComponentInterface,
0 ignored issues
show
Bug introduced by
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...
16
	Ratchet\Server\IoServer,
0 ignored issues
show
Bug introduced by
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...
17
	Ratchet\WebSocket\WsServer,
0 ignored issues
show
Bug introduced by
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...
18
	React\Dns\Resolver\Factory as Dns_factory,
0 ignored issues
show
Bug introduced by
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...
19
	React\EventLoop\Factory as Loop_factory,
0 ignored issues
show
Bug introduced by
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...
20
	cs\Config,
21
	cs\Event,
22
	cs\Request,
23
	cs\Session,
24
	cs\Singleton,
25
	cs\User,
26
	Exception,
27
	SplObjectStorage;
28
29
/**
30
 * @method static $this instance($check = false)
31
 */
32
class Server implements MessageComponentInterface {
33
	use
34
		Singleton;
35
	/**
36
	 * Message will be delivered to everyone
37
	 */
38
	const SEND_TO_ALL = 1;
39
	/**
40
	 * Message will be delivered to registered users only
41
	 */
42
	const SEND_TO_REGISTERED_USERS = 2;
43
	/**
44
	 * Message will be delivered to users, specified in target (might be array of users)
45
	 */
46
	const SEND_TO_SPECIFIC_USERS = 3;
47
	/**
48
	 * Message will be delivered to users from group, specified in target (might be array of groups)
49
	 */
50
	const SEND_TO_USERS_GROUP = 4;
51
	/**
52
	 * Message will be delivered to users whose connection objects have property with certain value, target should be an array with format [property, value]
53
	 */
54
	const SEND_TO_FILTER = 5;
55
	/**
56
	 * Each object additionally will have properties `user_id`, `session_id`, `session_expire` and `user_groups` with user id and ids of user groups
57
	 * correspondingly
58
	 *
59
	 * @var ConnectionInterface[]|SplObjectStorage
60
	 */
61
	protected $clients;
62
	/**
63
	 * @var ConnectionInterface[]|SplObjectStorage
64
	 */
65
	protected $servers;
66
	/**
67
	 * Connection to master server
68
	 *
69
	 * @var Client_websocket
70
	 */
71
	protected $connection_to_master;
72
	/**
73
	 * @var Pool
74
	 */
75
	protected $pool;
76
	/**
77
	 * Address to WebSockets server in format wss://server/WebSockets or ws://server/WebSockets, so that one WebSockets server can reach another (in case of
78
	 * several servers)
79
	 *
80
	 * @var string
81
	 */
82
	protected $address;
83
	/**
84
	 * @var IoServer
85
	 */
86
	protected $io_server;
87
	/**
88
	 * @var \React\EventLoop\LoopInterface
0 ignored issues
show
Bug introduced by
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...
89
	 */
90
	protected $loop;
91
	/**
92
	 * @var Client_connector
93
	 */
94
	protected $client_connector;
95
	/**
96
	 * @var int
97
	 */
98
	protected $listen_port;
99
	/**
100
	 * @var string
101
	 */
102
	protected $listen_locally;
103
	/**
104
	 * @var string
105
	 */
106
	protected $dns_server;
107
	/**
108
	 * @var string
109
	 */
110
	protected $security_key;
111
	/**
112
	 * @var bool
113
	 */
114
	protected $remember_session_ip;
115
	protected function construct () {
116
		$Config                    = Config::instance();
117
		$Request                   = Request::instance();
118
		$module_data               = $Config->module('WebSockets');
119
		$this->listen_port         = $module_data->listen_port;
0 ignored issues
show
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...
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...
120
		$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...
121
		$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...
122
		$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...
123
		$this->remember_session_ip = $Config->core['remember_user_ip'];
124
		$this->pool                = Pool::instance();
125
		$this->clients             = new SplObjectStorage;
126
		$this->servers             = new SplObjectStorage;
127
		$this->address             = ($Request->secure ? 'wss' : 'ws')."://$Request->host/WebSockets";
128
	}
129
	/**
130
	 * Run WebSockets server
131
	 *
132
	 * @param null|string $address
133
	 */
134
	public function run ($address = null) {
135
		$this->address = $address ?: $this->address;
136
		@ini_set('error_log', LOGS.'/WebSockets-server.log');
137
		$ws_server              = new WsServer($this);
138
		$this->io_server        = IoServer::factory(
139
			new HttpServer(
140
				new Connection_properties_injector($ws_server)
141
			),
142
			$this->listen_port,
143
			$this->listen_locally
144
		);
145
		$this->loop             = $this->io_server->loop;
146
		$this->client_connector = new Client_connector(
147
			$this->loop,
148
			(new Dns_factory)->create(
149
				$this->dns_server,
150
				$this->loop
151
			)
152
		);
153
		$this->connect_to_master();
154
		// Since we may work with a lot of different users - disable this cache in order to not run out of memory
155
		User::instance()->disable_memory_cache();
156
		$this->loop->run();
157
	}
158
	/**
159
	 * @param ConnectionInterface $connection
160
	 */
161
	public function onOpen (ConnectionInterface $connection) {
162
		$this->clients->attach($connection);
163
	}
164
	/**
165
	 * @param ConnectionInterface $connection
166
	 * @param string              $message
167
	 */
168
	public function onMessage (ConnectionInterface $connection, $message) {
169
		$this->on_message_internal($connection, $message);
170
	}
171
	/**
172
	 * TODO: Probably split this into 2 methods
173
	 *
174
	 * @param Client_websocket|ConnectionInterface $connection
175
	 * @param string                               $message
176
	 */
177
	protected function on_message_internal ($connection, $message) {
178
		$from_master = $connection === $this->connection_to_master;
179
		if (!$this->parse_message($message, $action, $details, $send_to, $target)) {
180
			if (!$from_master) {
181
				$connection->close();
182
			}
183
			return;
184
		}
185
		switch ($action) {
186
			/**
187
			 * Connection to master server as server (by default all connections considered as clients)
188
			 */
189
			case "Server/connect:$this->security_key":
190
				/**
191
				 * Under certain circumstances it may happen so that one server become available through multiple addresses,
192
				 * in this case we need to remove one of them from list of pools
193
				 */
194
				if ($details['from_slave'] === $this->address) {
195
					$this->pool->del($details['to_master']);
196
					$connection->close();
197
				} else {
198
					$this->clients->detach($connection);
199
					$this->servers->attach($connection);
200
				}
201
				return;
202
			/**
203
			 * Internal connection from application
204
			 */
205
			case "Application/Internal:$this->security_key":
206
				if ($this->parse_message($details, $action_, $details_, $send_to_, $target_)) {
207
					$connection->close();
208
					$this->send_to_clients($action_, $details_, $send_to_, $target_);
209
				}
210
				return;
211
			case 'Client/authentication':
212
				$Session = Session::instance();
213
				/** @noinspection PhpUndefinedFieldInspection */
214
				$session = $Session->get($connection->session_id);
215
				/** @noinspection PhpUndefinedFieldInspection */
216
				if (
217
					!$session ||
0 ignored issues
show
introduced by
The condition ! $session || ! $Session..._addr, $connection->ip) can never be false.
Loading history...
218
					!$Session->is_session_owner($session['id'], $connection->user_agent, $connection->remote_addr, $connection->ip)
219
				) {
220
					$connection->send(
221
						_json_encode(['Client/authentication:error', $this->compose_error(403)])
222
					);
223
					$connection->close();
224
					return;
225
				}
226
				$connection->user_id        = $session['user'];
227
				$connection->session_id     = $session['id'];
228
				$connection->session_expire = $session['expire'];
229
				$connection->groups         = User::instance()->get_groups($session['user']);
230
				$connection->send(
231
					_json_encode(['Client/authentication', 'ok'])
232
				);
233
		}
234
		if ($from_master) {
235
			$this->send_to_clients_internal($action, $details, $send_to, $target);
236
		} elseif ($this->servers->contains($connection)) {
237
			$this->broadcast_message_to_servers($message, $connection);
238
			if (!$send_to) {
239
				return;
240
			}
241
			$this->send_to_clients_internal($action, $details, $send_to, $target);
242
		} elseif (property_exists($connection, 'user_id')) {
243
			/** @noinspection PhpUndefinedFieldInspection */
244
			Event::instance()->fire(
245
				'WebSockets/message',
246
				[
247
					'action'     => $action,
248
					'details'    => $details,
249
					'language'   => $connection->language,
250
					'user'       => $connection->user_id,
251
					'session'    => $connection->session_id,
252
					'connection' => $connection
253
				]
254
			);
255
		}
256
	}
257
	/**
258
	 * @param string    $message
259
	 * @param string    $action
260
	 * @param mixed     $details
261
	 * @param int|int[] $send_to
262
	 * @param int       $target
263
	 *
264
	 * @return bool
265
	 */
266
	protected function parse_message ($message, &$action, &$details, &$send_to, &$target) {
267
		$decoded_message = _json_decode($message);
268
		if (
269
			!is_array($decoded_message) ||
270
			!array_key_exists(0, $decoded_message) ||
271
			!array_key_exists(1, $decoded_message)
272
		) {
273
			return false;
274
		}
275
		list($action, $details) = $decoded_message;
276
		$send_to = isset($decoded_message[2]) ? $decoded_message[2] : 0;
277
		$target  = isset($decoded_message[3]) ? $decoded_message[3] : 0;
278
		return true;
279
	}
280
	/**
281
	 * @param string                   $message
282
	 * @param ConnectionInterface|null $skip_server
283
	 */
284
	protected function broadcast_message_to_servers ($message, $skip_server = null) {
285
		foreach ($this->servers as $server) {
286
			if ($server === $skip_server) {
287
				continue;
288
			}
289
			$server->send($message);
290
		}
291
	}
292
	/**
293
	 * Compose error
294
	 *
295
	 * @param int         $error_code    HTTP status code
296
	 * @param null|string $error_message String representation of status code
297
	 *
298
	 * @return array Array to be passed as details to `::send_to_clients()`
299
	 */
300
	public function compose_error ($error_code, $error_message = null) {
301
		$error_message = $error_message ?: status_code_string($error_code);
302
		return [$error_code, $error_message];
303
	}
304
	/**
305
	 * Send request to client
306
	 *
307
	 * @param string          $action
308
	 * @param mixed           $details
309
	 * @param int             $send_to Constants `self::SEND_TO*` should be used here
310
	 * @param false|int|int[] $target  Id or array of ids in case of response to one or several users or groups
311
	 */
312
	public function send_to_clients ($action, $details, $send_to, $target = false) {
313
		$message = _json_encode([$action, $details, $send_to, $target]);
314
		/**
315
		 * If server running in current process
316
		 */
317
		if ($this->io_server) {
318
			if ($this->connection_to_master) {
319
				$this->connection_to_master->send($message);
320
			} else {
321
				$this->broadcast_message_to_servers($message);
322
			}
323
			$this->send_to_clients_internal($action, $details, $send_to, $target);
324
			return;
325
		}
326
		$servers = $this->pool->get_all();
327
		if ($servers) {
0 ignored issues
show
introduced by
The condition $servers can never be true.
Loading history...
328
			shuffle($servers);
329
			$loop      = Loop_factory::create();
330
			$connector = new Client_connector($loop);
331
			$connector($servers[0])->then(
332
				function (Client_websocket $connection) use ($message) {
333
					$connection->send(
334
						_json_encode(["Application/Internal:$this->security_key", $message])
335
					);
336
					// Connection will be closed by server itself, no need to stop loop here
337
				},
338
				function () use ($loop) {
339
					$loop->stop();
340
				}
341
			);
342
			$loop->run();
343
		}
344
	}
345
	/**
346
	 * Send request to client
347
	 *
348
	 * @param string                  $action
349
	 * @param mixed                   $details
350
	 * @param int                     $send_to Constants `self::SEND_TO_*` should be used here
351
	 * @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
352
	 */
353
	protected function send_to_clients_internal ($action, $details, $send_to, $target = false) {
354
		$message = _json_encode([$action, $details]);
355
		/**
356
		 * Special system actions
357
		 */
358
		switch ($action) {
359
			case 'Server/close_by_session':
360
				foreach ($this->clients as $client) {
361
					if ($client->session_id == $details) {
362
						$client->send(_json_encode('Server/close'));
363
						$client->close();
364
					}
365
				}
366
				return;
367
			case 'Server/close_by_user':
368
				foreach ($this->clients as $client) {
369
					if ($client->user_id == $details) {
370
						$client->send(_json_encode('Server/close'));
371
						$client->close();
372
					}
373
				}
374
				return;
375
		}
376
		switch ($send_to) {
377
			case self::SEND_TO_ALL:
378
				foreach ($this->clients as $client) {
379
					$client->send($message);
380
				}
381
				break;
382
			case self::SEND_TO_REGISTERED_USERS:
383
				foreach ($this->clients as $client) {
384
					if (isset($client->user_id)) {
385
						$this->send_to_client_if_not_expire($client, $message);
386
					}
387
				}
388
				break;
389
			case self::SEND_TO_SPECIFIC_USERS:
390
				$target = (array)$target;
391
				foreach ($this->clients as $client) {
392
					if (isset($client->user_id) && in_array($client->user_id, $target)) {
393
						$this->send_to_client_if_not_expire($client, $message);
394
					}
395
				}
396
				break;
397
			case self::SEND_TO_USERS_GROUP:
398
				$target = (array)$target;
399
				foreach ($this->clients as $client) {
400
					if (isset($client->user_groups) && array_intersect($client->user_groups, $target)) {
401
						$this->send_to_client_if_not_expire($client, $message);
402
					}
403
				}
404
				break;
405
			case self::SEND_TO_FILTER:
406
				list($property, $value) = $target;
407
				foreach ($this->clients as $client) {
408
					if (isset($client->$property) && $client->$property === $value) {
409
						$this->send_to_client_if_not_expire($client, $message);
410
					}
411
				}
412
				break;
413
		}
414
	}
415
	/**
416
	 * If session not expire - will send message, otherwise will disconnect
417
	 *
418
	 * @param ConnectionInterface $client
419
	 * @param string              $message
420
	 */
421
	protected function send_to_client_if_not_expire ($client, $message) {
422
		/** @noinspection PhpUndefinedFieldInspection */
423
		if ($client->session_expire >= time()) {
424
			$client->send($message);
425
		} else {
426
			$client->close();
427
		}
428
	}
429
	/**
430
	 * Close all client connections by specified session id
431
	 *
432
	 * @param string $session_id
433
	 */
434
	public function close_by_session ($session_id) {
435
		$this->send_to_clients('Server/close_by_session', $session_id, 0);
436
	}
437
	/**
438
	 * Close all client connections by specified user id
439
	 *
440
	 * @param string $user_id
441
	 */
442
	public function close_by_user ($user_id) {
443
		$this->send_to_clients('Server/close_by_user', $user_id, 0);
444
	}
445
	/**
446
	 * Connect to master server
447
	 *
448
	 * Two trials, if server do not respond twice - it will be removed from servers pool, and next server will become master
449
	 */
450
	protected function connect_to_master () {
451
		static $last_trial = '';
452
		// Add server to connections pool and connect to master if any
453
		$this->pool->add($this->address);
454
		$master = $this->pool->get_master();
455
		if ($master && $master != $this->address) {
0 ignored issues
show
introduced by
The condition $master && $master != $this->address can never be true.
Loading history...
456
			call_user_func($this->client_connector, $master)->then(
457
				function (Client_websocket $connection) use (&$last_trial, $master) {
458
					$last_trial                 = '';
459
					$this->connection_to_master = $connection;
460
					/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
461
					$connection
462
						->on(
463
							'message',
464
							function ($message) use ($connection) {
465
								$this->on_message_internal($connection, $message);
466
							}
467
						)
468
						->on(
469
							'error',
470
							function () use ($connection) {
471
								$connection->close();
472
							}
473
						)
474
						->on(
475
							'close',
476
							function () {
477
								$this->connection_to_master = null;
478
								$this->loop->addTimer(
479
									1,
480
									function () {
481
										$this->connect_to_master();
482
									}
483
								);
484
							}
485
						);
486
					/**
487
					 * Tell master that we are server also, not regular client
488
					 */
489
					$connection->send(
490
						_json_encode(
491
							[
492
								"Server/connect:$this->security_key",
493
								[
494
									'to_master'  => $master,
495
									'from_slave' => $this->address
496
								]
497
							]
498
						)
499
					);
500
				},
501
				function () use (&$last_trial, $master) {
502
					if ($last_trial == $master) {
503
						$this->pool->del($master);
504
					} else {
505
						$last_trial = $master;
506
					}
507
					$this->loop->addTimer(
508
						1,
509
						function () {
510
							$this->connect_to_master();
511
						}
512
					);
513
					$this->connect_to_master();
514
				}
515
			);
516
		} else {
517
			$last_trial = '';
518
			/**
519
			 * Sometimes other servers may loose connection with master server, so new master will be selected and we need to handle this nicely
520
			 */
521
			$this->loop->addTimer(
522
				30,
523
				function () {
524
					$this->connect_to_master();
525
				}
526
			);
527
		}
528
	}
529
	/**
530
	 * Get event loop instance
531
	 *
532
	 * @return \React\EventLoop\LoopInterface
533
	 */
534
	public function get_loop () {
535
		return $this->loop;
536
	}
537
	/**
538
	 * @param ConnectionInterface $connection
539
	 */
540
	public function onClose (ConnectionInterface $connection) {
541
		/**
542
		 * Generate pseudo-event when client is disconnected
543
		 */
544
		if (isset($connection->user_id) && $this->clients->contains($connection)) {
545
			/** @noinspection PhpUndefinedFieldInspection */
546
			Event::instance()->fire(
547
				'WebSockets/message',
548
				[
549
					'action'     => 'Client/disconnection',
550
					'details'    => null,
551
					'language'   => $connection->language,
552
					'user'       => $connection->user_id,
553
					'session'    => $connection->session_id,
554
					'connection' => $connection
555
				]
556
			);
557
		}
558
		// The connection is closed, remove it, as we can no longer send it messages
559
		$this->clients->detach($connection);
560
		$this->servers->detach($connection);
561
	}
562
	/**
563
	 * @param ConnectionInterface $connection
564
	 * @param Exception           $e
565
	 */
566
	public function onError (ConnectionInterface $connection, Exception $e) {
567
		$connection->close();
568
	}
569
	public function __destruct () {
570
		$this->pool->del($this->address);
571
	}
572
}
573