Passed
Push — master ( b4e552...a91d54 )
by y
03:05
created

AbstractSocket::getProtocol()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 2
b 0
f 0
nc 1
nop 0
dl 0
loc 2
rs 10
1
<?php
2
3
namespace Helix\Socket;
4
5
use InvalidArgumentException;
6
7
/**
8
 * Abstract parent to all sockets.
9
 */
10
abstract class AbstractSocket implements SocketInterface {
11
12
    /**
13
     * The socket's address family.
14
     *
15
     * @var int `AF_*`
16
     */
17
    protected $domain;
18
19
    /**
20
     * The socket's protocol.
21
     *
22
     * @var int
23
     */
24
    protected $protocol;
25
26
    /**
27
     * The underlying PHP resource.
28
     *
29
     * @var resource
30
     */
31
    protected $resource;
32
33
    /**
34
     * **Variadic.**
35
     * Creates an instance of the called class.
36
     *
37
     * @link https://php.net/socket_create
38
     *
39
     * @param int $domain `AF_*`
40
     * @param array $extra Variadic constructor arguments.
41
     * @return static
42
     * @throws Error
43
     */
44
    public static function create ($domain = AF_INET, ...$extra) {
45
        if (!$resource = @socket_create($domain, static::getType(), 0)) { // auto-protocol
46
            throw new Error; // reliable errno
47
        }
48
        return new static(...array_merge([$resource], $extra));
0 ignored issues
show
Bug introduced by
array_merge(array($resource), $extra) is expanded, but the parameter $resource of Helix\Socket\AbstractSocket::__construct() does not expect variable arguments. ( Ignorable by Annotation )

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

48
        return new static(/** @scrutinizer ignore-type */ ...array_merge([$resource], $extra));
Loading history...
49
    }
50
51
    /**
52
     * Selects instances.
53
     *
54
     * @link https://php.net/socket_select
55
     *
56
     * @param SocketInterface[] $read
57
     * @param SocketInterface[] $write
58
     * @param SocketInterface[] $except
59
     * @param float|null $timeout Maximum seconds to block. `NULL` blocks forever.
60
     * @return int
61
     * @throws Error
62
     */
63
    public static function select (array &$read, array &$write, array &$except, $timeout = null) {
64
        $getResource = function(SocketInterface $socket) {
65
            return $socket->getResource();
66
        };
67
        // PHP 7.1 broke by-reference array_walk_recursive, use array_map instead.
68
        $r = array_map($getResource, $read);
69
        $w = array_map($getResource, $write);
70
        $e = array_map($getResource, $except);
71
        $usec = (int)(fmod($timeout, 1) * 1000000); // ignored if timeout is null
72
        $count = @socket_select($r, $w, $e, $timeout, $usec);
0 ignored issues
show
Bug introduced by
It seems like $timeout can also be of type double; however, parameter $tv_sec of socket_select() 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

72
        $count = @socket_select($r, $w, $e, /** @scrutinizer ignore-type */ $timeout, $usec);
Loading history...
73
        if ($count === false) {
74
            $read = $write = $except = [];
75
            throw new Error;
76
        }
77
        $read = array_intersect_key($read, $r);
78
        $write = array_intersect_key($write, $w);
79
        $except = array_intersect_key($except, $e);
80
        return $count;
81
    }
82
83
    /**
84
     * Validates and sets the underlying socket resource.
85
     *
86
     * The resource must be open, of the correct type, and have no pending errors.
87
     *
88
     * @param resource $resource PHP socket resource.
89
     * @throws InvalidArgumentException Not a socket resource, or the socket is of the wrong type.
90
     * @throws Error (`SOCKET_EBADFD`) There was a pending error on the socket.
91
     * The preexisting error code is set as the exception's extra data.
92
     */
93
    public function __construct ($resource) {
94
        if (!is_resource($resource) or get_resource_type($resource) !== 'Socket') {
95
            throw new InvalidArgumentException('Expected an open socket resource.', SOCKET_EBADF);
96
        }
97
        elseif (socket_get_option($resource, SOL_SOCKET, SO_TYPE) !== static::getType()) {
98
            throw new InvalidArgumentException('The given socket resource is of the wrong type for ' . get_class(), SOCKET_ESOCKTNOSUPPORT);
99
        }
100
        elseif ($errno = Error::getLast($resource)) {
101
            $error = new Error(SOCKET_EBADFD);
102
            $error->setExtra($errno);
103
            throw $error;
104
        }
105
        $this->resource = $resource;
106
        $this->domain = $this->getOption(self::SO_DOMAIN);
107
        $this->protocol = $this->getOption(self::SO_PROTOCOL);
108
    }
109
110
    /**
111
     * Closes the socket if it's open.
112
     */
113
    public function __destruct () {
114
        $this->close();
115
    }
116
117
    /**
118
     * Blocks until the socket becomes available on a given channel.
119
     *
120
     * Throws if the underlying resource has a pending error (non-blocking).
121
     *
122
     * @see isReady()
123
     *
124
     * @param int $channel `SocketInterface` channel constant.
125
     * @return $this
126
     * @throws Error
127
     */
128
    public function await ($channel) {
129
        $select = [$channel => [$this->resource]];
130
        $count = @socket_select($select[self::CH_READ], $select[self::CH_WRITE], $select[self::CH_EXCEPT], null);
131
        if (!$count) {
132
            throw new Error($this->resource);
133
        }
134
        return $this;
135
    }
136
137
    /**
138
     * Closes the socket if it's open.
139
     * I/O channels should be shut down before calling this.
140
     *
141
     * @link https://php.net/socket_close
142
     *
143
     * @see onClose()
144
     *
145
     * @return $this
146
     */
147
    public function close () {
148
        // socket_close() never errors, but we guard anyway to prevent
149
        // a redundant call to onClose(), and an incorrect call to it if
150
        // the constructor failed and the destructor is calling this.
151
        if ($this->isOpen()) {
152
            $this->onClose();
153
            socket_close($this->resource);
154
        }
155
        return $this;
156
    }
157
158
    /**
159
     * @return int
160
     */
161
    final public function getDomain () {
162
        return $this->domain;
163
    }
164
165
    /**
166
     * @return int
167
     */
168
    final public function getId () {
169
        return (int)$this->resource;
170
    }
171
172
    /**
173
     * Retrieves a socket-level option.
174
     *
175
     * @link https://php.net/socket_get_option
176
     *
177
     * @param int $option `SO_*` option constant.
178
     * @throws Error
179
     * @return mixed The option's value. This is never `false`.
180
     */
181
    public function getOption ($option) {
182
        $value = @socket_get_option($this->resource, SOL_SOCKET, $option);
183
        if ($value === false) {
184
            throw new Error($this->resource, SOCKET_EINVAL);
185
        }
186
        return $value;
187
    }
188
189
    /**
190
     * @return int
191
     */
192
    final public function getProtocol () {
193
        return $this->protocol;
194
    }
195
196
    /**
197
     * @return resource
198
     */
199
    final public function getResource () {
200
        return $this->resource;
201
    }
202
203
    /**
204
     * Returns the local address and port, or Unix file path and port `0`.
205
     *
206
     * @link https://php.net/socket_getsockname
207
     *
208
     * @throws Error
209
     * @return array String address/file and integer port at indices `0` and `1`.
210
     */
211
    public function getSockName () {
212
        if (!@socket_getsockname($this->resource, $addr, $port)) {
213
            throw new Error($this->resource, SOCKET_EOPNOTSUPP);
214
        }
215
        return [$addr, $port];
216
    }
217
218
    /**
219
     * Whether the underlying resource is open.
220
     *
221
     * @return bool
222
     */
223
    public function isOpen () {
224
        return is_resource($this->resource);
225
    }
226
227
    /**
228
     * Polls for whether the socket can perform a non-blocking out-of-band read.
229
     *
230
     * @see isReady()
231
     *
232
     * @return bool
233
     * @throws Error
234
     */
235
    public function isOutOfBand () {
236
        return $this->isReady(self::CH_EXCEPT);
237
    }
238
239
    /**
240
     * Polls for whether the socket can perform a non-blocking read.
241
     *
242
     * @see isReady()
243
     *
244
     * @return bool
245
     * @throws Error
246
     */
247
    final public function isReadable () {
248
        return $this->isReady(self::CH_READ);
249
    }
250
251
    /**
252
     * Selects for channel availability.
253
     *
254
     * @see await()
255
     *
256
     * @param int $channel `SocketInterface` channel constant.
257
     * @param float|null $timeout Maximum seconds to block. `NULL` blocks forever. Defaults to a poll.
258
     * @return bool
259
     * @throws Error
260
     */
261
    public function isReady ($channel, $timeout = 0.0) {
262
        $usec = (int)(fmod($timeout, 1) * 1000000); // ignored if timeout is null
263
        $select = [$channel => [$this->resource]];
264
        $count = $count = @socket_select(
0 ignored issues
show
Unused Code introduced by
The assignment to $count is dead and can be removed.
Loading history...
265
            $select[self::CH_READ],
266
            $select[self::CH_WRITE],
267
            $select[self::CH_EXCEPT],
268
            $timeout, // allow null to pass through. core converts numeric to int.
0 ignored issues
show
Bug introduced by
It seems like $timeout can also be of type double; however, parameter $tv_sec of socket_select() 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

268
            /** @scrutinizer ignore-type */ $timeout, // allow null to pass through. core converts numeric to int.
Loading history...
269
            $usec
270
        );
271
        if ($count === false) {
272
            throw new Error($this->resource);
273
        }
274
        return (bool)$count;
275
    }
276
277
    /**
278
     * Polls for whether the socket can perform a non-blocking write.
279
     *
280
     * @see isReady()
281
     *
282
     * @return bool
283
     * @throws Error
284
     */
285
    final public function isWritable () {
286
        return $this->isReady(self::CH_WRITE);
287
    }
288
289
    /**
290
     * Stub. Called before the underlying resource is closed.
291
     *
292
     * @return void
293
     */
294
    protected function onClose () {
295
    }
296
297
    /**
298
     * Stub. Called before an I/O channel is shut down.
299
     *
300
     * @param int $channel `SocketInterface` channel constant.
301
     * @return void
302
     */
303
    protected function onShutdown ($channel) {
0 ignored issues
show
Unused Code introduced by
The parameter $channel is not used and could be removed. ( Ignorable by Annotation )

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

303
    protected function onShutdown (/** @scrutinizer ignore-unused */ $channel) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
304
    }
305
306
    /**
307
     * Stub. Called when an I/O timeout has occurred, before it's thrown.
308
     *
309
     * @return void
310
     */
311
    protected function onTimeout () {
312
    }
313
314
    /**
315
     * Enables or disables blocking.
316
     *
317
     * **Note: PHP provides no way to retrieve a socket's blocking mode.**
318
     *
319
     * Sockets are always created in blocking mode.
320
     *
321
     * Non-blocking errors are thrown when performing ordinary operations, even if unrelated to those operations.
322
     * This is known as error slippage.
323
     * To test for and clear an existing non-blocking error, use `->getOption(SO_ERROR)`.
324
     *
325
     * @link https://php.net/socket_set_block
326
     * @link https://php.net/socket_set_nonblock
327
     *
328
     * @param bool $blocking Whether the socket should block.
329
     * @throws Error
330
     * @return $this
331
     */
332
    public function setBlocking ($blocking) {
333
        if ($blocking) {
334
            $success = @socket_set_block($this->resource);
335
        }
336
        else {
337
            $success = @socket_set_nonblock($this->resource);
338
        }
339
        if (!$success) {
340
            throw new Error($this->resource); // reliable errno
341
        }
342
        return $this;
343
    }
344
345
    /**
346
     * Sets an option on the underlying resource.
347
     *
348
     * @link https://php.net/socket_set_option
349
     *
350
     * @param int $option `SO_*` constant.
351
     * @param mixed $value
352
     * @throws Error
353
     * @return $this
354
     */
355
    public function setOption ($option, $value) {
356
        if (!@socket_set_option($this->resource, SOL_SOCKET, $option, $value)) {
357
            throw new Error($this->resource, SOCKET_EINVAL);
358
        }
359
        return $this;
360
    }
361
362
    /**
363
     * Sets the I/O timeout length in seconds.
364
     *
365
     * @param float $timeout Zero means "no timeout" (block forever).
366
     * @return $this
367
     * @throws Error
368
     */
369
    public function setTimeout ($timeout) {
370
        $timeval = ['sec' => (int)$timeout, 'usec' => (int)(fmod($timeout, 1) * 1000000)];
371
        $this->setOption(SO_RCVTIMEO, $timeval);
372
        $this->setOption(SO_SNDTIMEO, $timeval);
373
        return $this;
374
    }
375
376
    /**
377
     * Shuts down I/O for a single channel.
378
     *
379
     * If writing is shut down, trying to send will throw `SOCKET_EPIPE`,
380
     * and the remote peer will read an empty string after receiving all pending data.
381
     *
382
     * If reading is shut down, trying to receive will return an empty string,
383
     * and the remote peer will get an `EPIPE` error if they try to send.
384
     *
385
     * Writing should be shut down first between two connected sockets.
386
     *
387
     * Selection always returns positive for a shut down channel,
388
     * and the remote peer will similarly have positive selection for the opposite channel.
389
     *
390
     * @link https://php.net/socket_shutdown
391
     *
392
     * @param int $channel `SocketInterface::CH_READ` or `SocketInterface::CH_WRITE`
393
     * @throws Error
394
     * @return $this
395
     */
396
    public function shutdown ($channel) {
397
        $this->onShutdown($channel);
398
        if (!@socket_shutdown($this->resource, $channel)) {
399
            throw new Error($this->resource); // reliable errno
400
        }
401
        return $this;
402
    }
403
404
}