Passed
Push — master ( d048db...e350b2 )
by Shahrad
01:41
created

WSConnectionTrait::reconnect()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 7
rs 10
c 1
b 0
f 0
1
<?php
2
3
namespace EasyHttp\Traits;
4
5
use EasyHttp\Contracts\CommonsContract;
6
use EasyHttp\Exceptions\BadOpcodeException;
7
use EasyHttp\Exceptions\ConnectionException;
8
use EasyHttp\Exceptions\WebSocketException;
9
use EasyHttp\WebSocketConfig;
10
11
/**
12
 * WSConnectionTrait class
13
 *
14
 * @link    https://github.com/shahradelahi/easy-http
15
 * @author  Shahrad Elahi (https://github.com/shahradelahi)
16
 * @license https://github.com/shahradelahi/easy-http/blob/master/LICENSE (MIT License)
17
 */
18
trait WSConnectionTrait
19
{
20
21
	/**
22
	 * @var callable|null
23
	 */
24
	public $onOpen = null;
25
26
	/**
27
	 * @var callable|null
28
	 */
29
	public $onClose = null;
30
31
	/**
32
	 * @var callable|null
33
	 */
34
	public $onError = null;
35
36
	/**
37
	 * @var callable|null
38
	 */
39
	public $onMessage = null;
40
41
	/**
42
	 * @var callable|null
43
	 */
44
	public $onWhile = null;
45
46
	/**
47
	 * @var bool
48
	 */
49
	private bool $isConnected = false;
50
51
	/**
52
	 * @var bool
53
	 */
54
	private bool $isClosing = false;
55
56
	/**
57
	 * Default headers
58
	 *
59
	 * @var array
60
	 */
61
	private array $defaultHeaders = [
62
		'Connection' => 'Upgrade',
63
		'Upgrade' => 'WebSocket',
64
		'Sec-Websocket-Version' => '13',
65
	];
66
67
	/**
68
	 * @param string $socketUrl string that represents the URL of the Web Socket server. e.g. ws://localhost:1337 or wss://localhost:1337
69
	 * @param ?WebSocketConfig $config The configuration for the Web Socket client
70
	 */
71
	public function connect(string $socketUrl, ?WebSocketConfig $config = null): void
72
	{
73
		try {
74
			$this->config = $config ?? new WebSocketConfig();
0 ignored issues
show
Bug Best Practice introduced by
The property config does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
75
			$this->socketUrl = $socketUrl;
0 ignored issues
show
Bug Best Practice introduced by
The property socketUrl does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
76
			$urlParts = parse_url($this->socketUrl);
77
78
			$this->config->setScheme($urlParts['scheme']);
79
			$this->config->setHost($urlParts['host']);
80
			$this->config->setUser($urlParts);
81
			$this->config->setPassword($urlParts);
82
			$this->config->setPort($urlParts);
83
84
			$pathWithQuery = $this->getPathWithQuery($urlParts);
0 ignored issues
show
Bug introduced by
It seems like getPathWithQuery() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

84
			/** @scrutinizer ignore-call */ 
85
   $pathWithQuery = $this->getPathWithQuery($urlParts);
Loading history...
85
			$hostUri = $this->getHostUri($this->config);
0 ignored issues
show
Bug introduced by
It seems like getHostUri() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

85
			/** @scrutinizer ignore-call */ 
86
   $hostUri = $this->getHostUri($this->config);
Loading history...
86
87
			$context = $this->getStreamContext();
0 ignored issues
show
Bug introduced by
It seems like getStreamContext() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

87
			/** @scrutinizer ignore-call */ 
88
   $context = $this->getStreamContext();
Loading history...
88
			if ($this->config->hasProxy()) {
89
				$this->socket = $this->proxy();
0 ignored issues
show
Bug Best Practice introduced by
The property socket does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug introduced by
It seems like proxy() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

89
				/** @scrutinizer ignore-call */ 
90
    $this->socket = $this->proxy();
Loading history...
90
			} else {
91
				$this->socket = @stream_socket_client(
92
					$hostUri . ':' . $this->config->getPort(),
93
					$errno,
94
					$errstr,
95
					$this->config->getTimeout(),
96
					STREAM_CLIENT_CONNECT,
97
					$context
98
				);
99
			}
100
101
			if ($this->socket === false) {
102
				throw new ConnectionException(
103
					"Could not open socket to \"{$this->config->getHost()}:{$this->config->getPort()}\": $errstr ($errno).",
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $errstr does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $errno does not seem to be defined for all execution paths leading up to this point.
Loading history...
104
					CommonsContract::CLIENT_COULD_NOT_OPEN_SOCKET
105
				);
106
			}
107
108
			stream_set_timeout($this->socket, $this->config->getTimeout());
109
110
			$key = $this->generateKey();
0 ignored issues
show
Bug introduced by
It seems like generateKey() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

110
			/** @scrutinizer ignore-call */ 
111
   $key = $this->generateKey();
Loading history...
111
			$headers = array_merge($this->defaultHeaders, [
112
				'Host' => $this->config->getHost() . ':' . $this->config->getPort(),
113
				'User-Agent' => 'Easy-Http/' . self::VERSION . ' (PHP/' . PHP_VERSION . ')',
0 ignored issues
show
Bug introduced by
The constant EasyHttp\Traits\WSConnectionTrait::VERSION was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
114
				'Sec-WebSocket-Key' => $key,
115
			]);
116
117
			if ($this->config->getUser() || $this->config->getPassword()) {
118
				$headers['authorization'] = 'Basic ' . base64_encode($this->config->getUser() . ':' . $this->config->getPassword()) . "\r\n";
119
			}
120
121
			if (!empty($this->config->getHeaders())) {
122
				$headers = array_merge($headers, $this->config->getHeaders());
123
			}
124
125
			$header = $this->getHeaders($pathWithQuery, $headers);
0 ignored issues
show
Bug introduced by
It seems like getHeaders() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

125
			/** @scrutinizer ignore-call */ 
126
   $header = $this->getHeaders($pathWithQuery, $headers);
Loading history...
126
127
			$this->write($header);
0 ignored issues
show
Bug introduced by
It seems like write() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

127
			$this->/** @scrutinizer ignore-call */ 
128
          write($header);
Loading history...
128
129
			$this->validateResponse($this->config, $pathWithQuery, $key);
0 ignored issues
show
Bug introduced by
It seems like validateResponse() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

129
			$this->/** @scrutinizer ignore-call */ 
130
          validateResponse($this->config, $pathWithQuery, $key);
Loading history...
130
			$this->isConnected = true;
131
			$this->whileIsConnected();
132
133
		} catch (\Exception $e) {
134
			$this->safeCall($this->onError, $this, new WebSocketException(
135
				$e->getMessage(),
136
				$e->getCode(),
137
				$e->getPrevious()
138
			));
139
		}
140
	}
141
142
	/**
143
	 * Reconnect to the Web Socket server
144
	 *
145
	 * @return void
146
	 * @throws \Exception
147
	 */
148
	public function reconnect(): void
149
	{
150
		if ($this->isConnected) {
151
			$this->close();
152
		}
153
154
		$this->connect($this->socketUrl, $this->config);
155
	}
156
157
	/**
158
	 * @return void
159
	 * @throws WebSocketException|\Exception
160
	 */
161
	private function whileIsConnected(): void
162
	{
163
		$this->safeCall($this->onOpen, $this);
164
165
		while ($this->isConnected()) {
0 ignored issues
show
Bug introduced by
It seems like isConnected() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

165
		while ($this->/** @scrutinizer ignore-call */ isConnected()) {
Loading history...
166
			$this->safeCall($this->onWhile, $this);
167
168
			if (is_string(($message = $this->receive()))) {
169
				$this->safeCall($this->onMessage, $this, $message);
170
			}
171
		}
172
173
		$this->safeCall($this->onClose, $this, $this->closeStatus);
174
	}
175
176
	/**
177
	 * Execute events with safety of exceptions
178
	 *
179
	 * @param callable|null $callback
180
	 * @param mixed ...$args
181
	 * @return void
182
	 */
183
	private function safeCall(?callable $callback, ...$args): void
184
	{
185
		if (is_callable($callback) && $callback) {
186
			call_user_func($callback, ...$args);
187
		}
188
	}
189
190
	/**
191
	 * Sends message to opened socket connection client->server
192
	 *
193
	 * @param $payload
194
	 * @param string $opcode
195
	 * @throws \Exception
196
	 */
197
	public function send($payload, string $opcode = CommonsContract::EVENT_TYPE_TEXT): void
198
	{
199
		if (!$this->isConnected) {
200
			throw new \Exception(
201
				"Can't send message. Connection is not established.",
202
				CommonsContract::CLIENT_CONNECTION_NOT_ESTABLISHED
203
			);
204
		}
205
206
		if (array_key_exists($opcode, self::$opcodes) === false) {
207
			throw new BadOpcodeException(
208
				sprintf("Bad opcode '%s'.  Try 'text' or 'binary'.", $opcode),
209
				CommonsContract::CLIENT_BAD_OPCODE
210
			);
211
		}
212
213
		$payloadLength = strlen($payload);
214
		$fragmentCursor = 0;
215
216
		while ($payloadLength > $fragmentCursor) {
217
			$subPayload = substr($payload, $fragmentCursor, $this->config->getFragmentSize());
218
			$fragmentCursor += $this->config->getFragmentSize();
219
			$final = $payloadLength <= $fragmentCursor;
220
			$this->sendFragment($final, $subPayload, $opcode, true);
0 ignored issues
show
Bug introduced by
It seems like sendFragment() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

220
			$this->/** @scrutinizer ignore-call */ 
221
          sendFragment($final, $subPayload, $opcode, true);
Loading history...
221
			$opcode = 'continuation';
222
		}
223
	}
224
225
	/**
226
	 * Receives message client<-server
227
	 *
228
	 * @return string|null
229
	 * @throws \Exception
230
	 */
231
	public function receive(): string|null
232
	{
233
		if (!$this->isConnected && $this->isClosing === false) {
234
			throw new WebSocketException(
235
				"Your unexpectedly disconnected from the server",
236
				CommonsContract::CLIENT_CONNECTION_NOT_ESTABLISHED
237
			);
238
		}
239
240
		$this->hugePayload = '';
0 ignored issues
show
Bug Best Practice introduced by
The property hugePayload does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
241
242
		return $this->receiveFragment();
0 ignored issues
show
Bug introduced by
The method receiveFragment() does not exist on EasyHttp\Traits\WSConnectionTrait. Did you maybe mean receive()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

242
		return $this->/** @scrutinizer ignore-call */ receiveFragment();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
243
	}
244
245
	/**
246
	 * Tell the socket to close.
247
	 *
248
	 * @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4
249
	 * @param string $message A closing message, max 125 bytes.
250
	 * @return bool|null|string
251
	 * @throws \Exception
252
	 */
253
	public function close(int $status = 1000, string $message = 'ttfn'): bool|null|string
254
	{
255
		$statusBin = sprintf('%016b', $status);
256
		$statusStr = '';
257
258
		foreach (str_split($statusBin, 8) as $binstr) {
259
			$statusStr .= chr(bindec($binstr));
0 ignored issues
show
Bug introduced by
It seems like bindec($binstr) can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

259
			$statusStr .= chr(/** @scrutinizer ignore-type */ bindec($binstr));
Loading history...
260
		}
261
262
		$this->send($statusStr . $message, CommonsContract::EVENT_TYPE_CLOSE);
263
		$this->isClosing = true;
264
265
		return $this->receive(); // Receiving a close frame will close the socket now.
266
	}
267
268
}