1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace WSSC\Components; |
4
|
|
|
|
5
|
|
|
use WSSC\Contracts\CommonsContract; |
6
|
|
|
use WSSC\Contracts\WscCommonsContract; |
7
|
|
|
use WSSC\Exceptions\BadOpcodeException; |
8
|
|
|
use WSSC\Exceptions\BadUriException; |
9
|
|
|
use WSSC\Exceptions\ConnectionException; |
10
|
|
|
|
11
|
|
|
class WscMain implements WscCommonsContract |
12
|
|
|
{ |
13
|
|
|
|
14
|
|
|
private $socket; |
15
|
|
|
private $isConnected = false; |
16
|
|
|
private $isClosing = false; |
17
|
|
|
private $lastOpcode; |
18
|
|
|
private $closeStatus; |
19
|
|
|
private $hugePayload; |
20
|
|
|
|
21
|
|
|
private static $opcodes = [ |
22
|
|
|
CommonsContract::EVENT_TYPE_CONTINUATION => 0, |
23
|
|
|
CommonsContract::EVENT_TYPE_TEXT => 1, |
24
|
|
|
CommonsContract::EVENT_TYPE_BINARY => 2, |
25
|
|
|
CommonsContract::EVENT_TYPE_CLOSE => 8, |
26
|
|
|
CommonsContract::EVENT_TYPE_PING => 9, |
27
|
|
|
CommonsContract::EVENT_TYPE_PONG => 10, |
28
|
|
|
]; |
29
|
|
|
|
30
|
|
|
protected $socketUrl = ''; |
31
|
|
|
protected $options = []; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @throws \InvalidArgumentException |
35
|
|
|
* @throws BadUriException |
36
|
|
|
* @throws ConnectionException |
37
|
|
|
* @throws \Exception |
38
|
|
|
*/ |
39
|
|
|
protected function connect() : void |
40
|
|
|
{ |
41
|
|
|
$urlParts = parse_url($this->socketUrl); |
42
|
|
|
$scheme = $urlParts['scheme']; |
43
|
|
|
$host = $urlParts['host']; |
44
|
|
|
$user = isset($urlParts['user']) ? $urlParts['user'] : ''; |
45
|
|
|
$pass = isset($urlParts['pass']) ? $urlParts['pass'] : ''; |
46
|
|
|
$port = isset($urlParts['port']) ? $urlParts['port'] : ($scheme === 'wss' ? 443 : 80); |
47
|
|
|
|
48
|
|
|
$pathWithQuery = $this->getPathWithQuery($urlParts); |
|
|
|
|
49
|
|
|
if (in_array($scheme, ['ws', 'wss'], true) === false) { |
50
|
|
|
throw new BadUriException( |
51
|
|
|
"Url should have scheme ws or wss, not '$scheme' from URI '$this->socketUrl' ." |
52
|
|
|
); |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
$hostUri = ($scheme === 'wss' ? 'ssl' : 'tcp') . '://' . $host; |
56
|
|
|
// Set the stream context options if they're already set in the config |
57
|
|
|
$context = $this->getStreamContext(); |
58
|
|
|
$this->socket = @stream_socket_client( |
59
|
|
|
$hostUri . ':' . $port, $errno, $errstr, $this->options['timeout'], STREAM_CLIENT_CONNECT, $context |
60
|
|
|
); |
61
|
|
|
if ($this->socket === false) { |
62
|
|
|
throw new ConnectionException( |
63
|
|
|
"Could not open socket to \"$host:$port\": $errstr ($errno)." |
64
|
|
|
); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
// Set timeout on the stream as well. |
68
|
|
|
stream_set_timeout($this->socket, $this->options['timeout']); |
69
|
|
|
|
70
|
|
|
// Generate the WebSocket key. |
71
|
|
|
$key = $this->generateKey(); |
72
|
|
|
$headers = [ |
73
|
|
|
'Host' => $host . ':' . $port, |
74
|
|
|
'User-Agent' => 'websocket-client-php', |
75
|
|
|
'Connection' => 'Upgrade', |
76
|
|
|
'Upgrade' => 'WebSocket', |
77
|
|
|
'Sec-WebSocket-Key' => $key, |
78
|
|
|
'Sec-Websocket-Version' => '13', |
79
|
|
|
]; |
80
|
|
|
|
81
|
|
|
// Handle basic authentication. |
82
|
|
|
if ($user || $pass) { |
83
|
|
|
$headers['authorization'] = 'Basic ' . base64_encode($user . ':' . $pass) . "\r\n"; |
84
|
|
|
} |
85
|
|
|
// Add and override with headers from options. |
86
|
|
|
if (isset($this->options['headers'])) { |
87
|
|
|
$headers = array_merge($headers, $this->options['headers']); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
$header = $this->getHeaders($pathWithQuery, $headers); |
91
|
|
|
// Send headers. |
92
|
|
|
$this->write($header); |
93
|
|
|
// Get server response header |
94
|
|
|
$response = stream_get_line($this->socket, self::DEFAULT_RESPONSE_HEADER, "\r\n\r\n"); |
95
|
|
|
/// @todo Handle version switching |
96
|
|
|
// Validate response. |
97
|
|
|
if (!preg_match(self::SEC_WEBSOCKET_ACCEPT_PTTRN, $response, $matches)) { |
98
|
|
|
$address = $scheme . '://' . $host . $pathWithQuery; |
99
|
|
|
throw new ConnectionException( |
100
|
|
|
"Connection to '{$address}' failed: Server sent invalid upgrade response:\n" |
101
|
|
|
. $response |
102
|
|
|
); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
$keyAccept = trim($matches[1]); |
106
|
|
|
$expectedResonse = base64_encode(pack('H*', sha1($key . self::SERVER_KEY_ACCEPT))); |
107
|
|
|
if ($keyAccept !== $expectedResonse) { |
108
|
|
|
throw new ConnectionException('Server sent bad upgrade response.'); |
109
|
|
|
} |
110
|
|
|
$this->isConnected = true; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* @return mixed|resource |
115
|
|
|
* @throws \InvalidArgumentException |
116
|
|
|
*/ |
117
|
|
|
private function getStreamContext() |
118
|
|
|
{ |
119
|
|
|
if (isset($this->options['context'])) { |
120
|
|
|
// Suppress the error since we'll catch it below |
121
|
|
|
if (@get_resource_type($this->options['context']) === 'stream-context') { |
122
|
|
|
return $this->options['context']; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
throw new \InvalidArgumentException( |
126
|
|
|
"Stream context in \$options['context'] isn't a valid context" |
127
|
|
|
); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
return stream_context_create(); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
private function getPathWithQuery(array $urlParts) : string |
134
|
|
|
{ |
135
|
|
|
$path = isset($urlParts['path']) ? $urlParts['path'] : '/'; |
136
|
|
|
$query = isset($urlParts['query']) ? $urlParts['query'] : ''; |
137
|
|
|
$fragment = isset($urlParts['fragment']) ? $urlParts['fragment'] : ''; |
138
|
|
|
$pathWithQuery = $path; |
139
|
|
|
if (!empty($query)) { |
140
|
|
|
$pathWithQuery .= '?' . $query; |
141
|
|
|
} |
142
|
|
|
if (!empty($fragment)) { |
143
|
|
|
$pathWithQuery .= '#' . $fragment; |
144
|
|
|
} |
145
|
|
|
return $pathWithQuery; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* @param string $pathWithQuery |
150
|
|
|
* @param array $headers |
151
|
|
|
* @return string |
152
|
|
|
*/ |
153
|
|
|
private function getHeaders(string $pathWithQuery, array $headers) : string |
154
|
|
|
{ |
155
|
|
|
return 'GET ' . $pathWithQuery . " HTTP/1.1\r\n" |
156
|
|
|
. implode( |
157
|
|
|
"\r\n", array_map( |
158
|
|
|
function ($key, $value) { |
159
|
|
|
return "$key: $value"; |
160
|
|
|
}, array_keys($headers), $headers |
161
|
|
|
) |
162
|
|
|
) |
163
|
|
|
. "\r\n\r\n"; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* @return string |
168
|
|
|
*/ |
169
|
|
|
public function getLastOpcode() : string |
170
|
|
|
{ |
171
|
|
|
return $this->lastOpcode; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* @return int |
176
|
|
|
*/ |
177
|
|
|
public function getCloseStatus() : int |
178
|
|
|
{ |
179
|
|
|
return $this->closeStatus; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* @return bool |
184
|
|
|
*/ |
185
|
|
|
public function isConnected() : bool |
186
|
|
|
{ |
187
|
|
|
return $this->isConnected; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* @param int $timeout |
192
|
|
|
* @param null $microSecs |
193
|
|
|
*/ |
194
|
|
|
public function setTimeout(int $timeout, $microSecs = null) |
195
|
|
|
{ |
196
|
|
|
$this->options['timeout'] = $timeout; |
197
|
|
|
|
198
|
|
|
if ($this->socket && get_resource_type($this->socket) === 'stream') { |
199
|
|
|
stream_set_timeout($this->socket, $timeout, $microSecs); |
200
|
|
|
} |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
public function setFragmentSize($fragmentSize) |
204
|
|
|
{ |
205
|
|
|
$this->options['fragment_size'] = $fragmentSize; |
206
|
|
|
return $this; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
public function getFragmentSize() |
210
|
|
|
{ |
211
|
|
|
return $this->options['fragment_size']; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
public function send($payload, $opcode = 'text', $masked = true) |
215
|
|
|
{ |
216
|
|
|
if (!$this->isConnected) { |
217
|
|
|
$this->connect(); |
218
|
|
|
} |
219
|
|
|
if (array_key_exists($opcode, self::$opcodes) === false) { |
220
|
|
|
throw new BadOpcodeException("Bad opcode '$opcode'. Try 'text' or 'binary'."); |
221
|
|
|
} |
222
|
|
|
echo $payload; |
223
|
|
|
// record the length of the payload |
224
|
|
|
$payload_length = strlen($payload); |
225
|
|
|
|
226
|
|
|
$fragment_cursor = 0; |
227
|
|
|
// while we have data to send |
228
|
|
|
while ($payload_length > $fragment_cursor) { |
229
|
|
|
// get a fragment of the payload |
230
|
|
|
$sub_payload = substr($payload, $fragment_cursor, $this->options['fragment_size']); |
231
|
|
|
|
232
|
|
|
// advance the cursor |
233
|
|
|
$fragment_cursor += $this->options['fragment_size']; |
234
|
|
|
|
235
|
|
|
// is this the final fragment to send? |
236
|
|
|
$final = $payload_length <= $fragment_cursor; |
237
|
|
|
|
238
|
|
|
// send the fragment |
239
|
|
|
$this->sendFragment($final, $sub_payload, $opcode, $masked); |
240
|
|
|
|
241
|
|
|
// all fragments after the first will be marked a continuation |
242
|
|
|
$opcode = 'continuation'; |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* @param $final |
248
|
|
|
* @param $payload |
249
|
|
|
* @param $opcode |
250
|
|
|
* @param $masked |
251
|
|
|
* @throws ConnectionException |
252
|
|
|
* @throws \Exception |
253
|
|
|
*/ |
254
|
|
|
protected function sendFragment($final, $payload, $opcode, $masked) |
255
|
|
|
{ |
256
|
|
|
// Binary string for header. |
257
|
|
|
$frameHeadBin = ''; |
258
|
|
|
// Write FIN, final fragment bit. |
259
|
|
|
$frameHeadBin .= (bool)$final ? '1' : '0'; |
260
|
|
|
// RSV 1, 2, & 3 false and unused. |
|
|
|
|
261
|
|
|
$frameHeadBin .= '000'; |
262
|
|
|
// Opcode rest of the byte. |
263
|
|
|
$frameHeadBin .= sprintf('%04b', self::$opcodes[$opcode]); |
264
|
|
|
// Use masking? |
265
|
|
|
$frameHeadBin .= $masked ? '1' : '0'; |
266
|
|
|
|
267
|
|
|
// 7 bits of payload length... |
268
|
|
|
$payloadLen = strlen($payload); |
269
|
|
|
if ($payloadLen > self::MAX_BYTES_READ) { |
270
|
|
|
$frameHeadBin .= decbin(self::MASK_127); |
271
|
|
|
$frameHeadBin .= sprintf('%064b', $payloadLen); |
272
|
|
|
} else if ($payloadLen > self::MASK_125) { |
273
|
|
|
$frameHeadBin .= decbin(self::MASK_126); |
274
|
|
|
$frameHeadBin .= sprintf('%016b', $payloadLen); |
275
|
|
|
} else { |
276
|
|
|
$frameHeadBin .= sprintf('%07b', $payloadLen); |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
$frame = ''; |
280
|
|
|
|
281
|
|
|
// Write frame head to frame. |
282
|
|
|
foreach (str_split($frameHeadBin, 8) as $binstr) { |
283
|
|
|
$frame .= chr(bindec($binstr)); |
284
|
|
|
} |
285
|
|
|
// Handle masking |
286
|
|
|
if ($masked) { |
287
|
|
|
// generate a random mask: |
288
|
|
|
$mask = ''; |
289
|
|
View Code Duplication |
for ($i = 0; $i < 4; $i++) { |
|
|
|
|
290
|
|
|
$mask .= chr(random_int(0, 255)); |
291
|
|
|
} |
292
|
|
|
$frame .= $mask; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
// Append payload to frame: |
296
|
|
View Code Duplication |
for ($i = 0; $i < $payloadLen; $i++) { |
|
|
|
|
297
|
|
|
$frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; |
|
|
|
|
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
$this->write($frame); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
public function receive() |
304
|
|
|
{ |
305
|
|
|
if (!$this->isConnected) { |
306
|
|
|
$this->connect(); |
307
|
|
|
} |
308
|
|
|
$this->hugePayload = ''; |
309
|
|
|
|
310
|
|
|
$response = NULL; |
311
|
|
|
while (NULL === $response) { |
312
|
|
|
$response = $this->receiveFragment(); |
313
|
|
|
} |
314
|
|
|
return $response; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* @return string |
319
|
|
|
* @throws BadOpcodeException |
320
|
|
|
* @throws ConnectionException |
321
|
|
|
*/ |
322
|
|
|
protected function receiveFragment() : string |
323
|
|
|
{ |
324
|
|
|
// Just read the main fragment information first. |
325
|
|
|
$data = $this->read(2); |
326
|
|
|
|
327
|
|
|
// Is this the final fragment? // Bit 0 in byte 0 |
328
|
|
|
/// @todo Handle huge payloads with multiple fragments. |
329
|
|
|
$final = (bool)(ord($data[0]) & 1 << 7); |
330
|
|
|
|
331
|
|
|
// Parse opcode |
332
|
|
|
$opcode_int = ord($data[0]) & 31; // Bits 4-7 |
333
|
|
|
$opcode_ints = array_flip(self::$opcodes); |
334
|
|
|
if (!array_key_exists($opcode_int, $opcode_ints)) { |
335
|
|
|
throw new ConnectionException("Bad opcode in websocket frame: $opcode_int"); |
336
|
|
|
} |
337
|
|
|
$opcode = $opcode_ints[$opcode_int]; |
338
|
|
|
|
339
|
|
|
// record the opcode if we are not receiving a continutation fragment |
340
|
|
|
if ($opcode !== 'continuation') { |
341
|
|
|
$this->lastOpcode = $opcode; |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
// Masking? |
345
|
|
|
$mask = (bool)(ord($data[1]) >> 7); // Bit 0 in byte 1 |
346
|
|
|
|
347
|
|
|
$payload = ''; |
348
|
|
|
|
349
|
|
|
// Payload length |
350
|
|
|
$payload_length = (int)ord($data[1]) & self::MASK_127; // Bits 1-7 in byte 1 |
351
|
|
|
if ($payload_length > self::MASK_125) { |
352
|
|
|
if ($payload_length === self::MASK_126) { |
353
|
|
|
$data = $this->read(2); // 126: Payload is a 16-bit unsigned int |
354
|
|
|
} else { |
355
|
|
|
$data = $this->read(8); // 127: Payload is a 64-bit unsigned int |
356
|
|
|
} |
357
|
|
|
$payload_length = bindec(self::sprintB($data)); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
$maskingKey = ''; |
361
|
|
|
// Get masking key. |
362
|
|
|
if ($mask) { |
363
|
|
|
$maskingKey = $this->read(4); |
364
|
|
|
} |
365
|
|
|
// Get the actual payload, if any (might not be for e.g. close frames. |
366
|
|
|
if ($payload_length > 0) { |
367
|
|
|
$data = $this->read($payload_length); |
368
|
|
|
|
369
|
|
|
if ($mask) { |
370
|
|
|
// Unmask payload. |
371
|
|
|
for ($i = 0; $i < $payload_length; $i++) { |
372
|
|
|
$payload .= ($data[$i] ^ $maskingKey[$i % 4]); |
373
|
|
|
} |
374
|
|
|
} else { |
375
|
|
|
$payload = $data; |
376
|
|
|
} |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
if ($opcode === CommonsContract::EVENT_TYPE_CLOSE) { |
380
|
|
|
// Get the close status. |
381
|
|
|
if ($payload_length >= 2) { |
382
|
|
|
$status_bin = $payload[0] . $payload[1]; |
383
|
|
|
$status = bindec(sprintf('%08b%08b', ord($payload[0]), ord($payload[1]))); |
384
|
|
|
$this->closeStatus = $status; |
385
|
|
|
$payload = substr($payload, 2); |
386
|
|
|
|
387
|
|
|
if (!$this->isClosing) { |
388
|
|
|
$this->send($status_bin . 'Close acknowledged: ' . $status, 'close'); // Respond. |
389
|
|
|
} |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
if ($this->isClosing) { |
393
|
|
|
$this->isClosing = false; // A close response, all done. |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
fclose($this->socket); |
397
|
|
|
$this->isConnected = false; |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
if (!$final) { |
401
|
|
|
$this->hugePayload .= $payload; |
402
|
|
|
return NULL; |
403
|
|
|
} // this is the last fragment, and we are processing a huge_payload |
404
|
|
|
|
405
|
|
|
if ($this->hugePayload) { |
406
|
|
|
$payload = $this->hugePayload .= $payload; |
407
|
|
|
$this->hugePayload = NULL; |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
return $payload; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
/** |
414
|
|
|
* Tell the socket to close. |
415
|
|
|
* |
416
|
|
|
* @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4 |
417
|
|
|
* @param string $message A closing message, max 125 bytes. |
418
|
|
|
* @return bool|null|string |
419
|
|
|
* @throws BadOpcodeException |
420
|
|
|
*/ |
421
|
|
|
public function close(int $status = 1000, string $message = 'ttfn') |
422
|
|
|
{ |
423
|
|
|
$statusBin = sprintf('%016b', $status); |
424
|
|
|
$status_str = ''; |
425
|
|
|
foreach (str_split($statusBin, 8) as $binstr) { |
426
|
|
|
$status_str .= chr(bindec($binstr)); |
427
|
|
|
} |
428
|
|
|
$this->send($status_str . $message, 'close', true); |
429
|
|
|
$this->isClosing = true; |
430
|
|
|
return $this->receive(); // Receiving a close frame will close the socket now. |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* @param $data |
435
|
|
|
* @throws ConnectionException |
436
|
|
|
*/ |
437
|
|
|
protected function write(string $data) : void |
438
|
|
|
{ |
439
|
|
|
$written = fwrite($this->socket, $data); |
440
|
|
|
|
441
|
|
|
if ($written < strlen($data)) { |
442
|
|
|
throw new ConnectionException( |
443
|
|
|
"Could only write $written out of " . strlen($data) . " bytes." |
444
|
|
|
); |
445
|
|
|
} |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
/** |
449
|
|
|
* @param int $len |
450
|
|
|
* @return string |
451
|
|
|
* @throws ConnectionException |
452
|
|
|
*/ |
453
|
|
|
protected function read(int $len) : string |
454
|
|
|
{ |
455
|
|
|
$data = ''; |
456
|
|
|
while (($dataLen = strlen($data)) < $len) { |
457
|
|
|
$buff = fread($this->socket, $len - $dataLen); |
458
|
|
|
if ($buff === false) { |
459
|
|
|
$metadata = stream_get_meta_data($this->socket); |
460
|
|
|
throw new ConnectionException( |
461
|
|
|
'Broken frame, read ' . strlen($data) . ' of stated ' |
462
|
|
|
. $len . ' bytes. Stream state: ' |
463
|
|
|
. json_encode($metadata) |
464
|
|
|
); |
465
|
|
|
} |
466
|
|
|
if ($buff === '') { |
467
|
|
|
$metadata = stream_get_meta_data($this->socket); |
468
|
|
|
throw new ConnectionException( |
469
|
|
|
'Empty read; connection dead? Stream state: ' . json_encode($metadata) |
470
|
|
|
); |
471
|
|
|
} |
472
|
|
|
$data .= $buff; |
473
|
|
|
} |
474
|
|
|
return $data; |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
/** |
478
|
|
|
* Helper to convert a binary to a string of '0' and '1'. |
479
|
|
|
* |
480
|
|
|
* @param $string |
481
|
|
|
* @return string |
482
|
|
|
*/ |
483
|
|
|
protected static function sprintB(string $string) : string |
484
|
|
|
{ |
485
|
|
|
$return = ''; |
486
|
|
|
$strLen = strlen($string); |
487
|
|
|
for ($i = 0; $i < $strLen; $i++) { |
488
|
|
|
$return .= sprintf('%08b', ord($string[$i])); |
489
|
|
|
} |
490
|
|
|
return $return; |
491
|
|
|
} |
492
|
|
|
|
493
|
|
|
/** |
494
|
|
|
* Sec-WebSocket-Key generator |
495
|
|
|
* |
496
|
|
|
* @return string the 16 character length key |
497
|
|
|
* @throws \Exception |
498
|
|
|
*/ |
499
|
|
|
private function generateKey() : string |
500
|
|
|
{ |
501
|
|
|
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$&/()=[]{}0123456789'; |
502
|
|
|
$key = ''; |
503
|
|
|
$chLen = strlen($chars); |
504
|
|
|
for ($i = 0; $i < self::KEY_GEN_LENGTH; $i++) { |
505
|
|
|
$key .= $chars[random_int(0, $chLen - 1)]; |
506
|
|
|
} |
507
|
|
|
return base64_encode($key); |
508
|
|
|
} |
509
|
|
|
|
510
|
|
|
} |
511
|
|
|
|
This check looks for type mismatches where the missing type is
false
. This is usually indicative of an error condtion.Consider the follow example
This function either returns a new
DateTime
object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returnedfalse
before passing on the value to another function or method that may not be able to handle afalse
.