Issues (60)

src/Traits/WSConnectionTrait.php (20 issues)

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
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
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
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...
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
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
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
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
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
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() && $this->isClosing === false) {
0 ignored issues
show
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() && $this->isClosing === false) {
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
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
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 https://github.com/Luka967/websocket-close-codes
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
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->closeStatus = $status;
0 ignored issues
show
Bug Best Practice introduced by
The property closeStatus does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
264
		$this->isClosing = true;
265
266
		return $this->receive(); // Receiving a close frame will close the socket now.
267
	}
268
269
}