Passed
Push — master ( 83603b...b4e552 )
by y
01:30
created

AbstractSocket::getType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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
     * Polls for whether the socket can perform a non-blocking out-of-band read.
220
     *
221
     * @see isReady()
222
     *
223
     * @return bool
224
     * @throws Error
225
     */
226
    public function isExceptional () {
227
        return $this->isReady(self::CH_EXCEPT);
228
    }
229
230
    /**
231
     * Whether the underlying resource is open.
232
     *
233
     * @return bool
234
     */
235
    public function isOpen () {
236
        return is_resource($this->resource);
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
    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
     * @throws Error
259
     * @return bool
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($select[self::CH_READ], $select[self::CH_WRITE], $select[self::CH_EXCEPT], $timeout, $usec);
0 ignored issues
show
Unused Code introduced by
The assignment to $count is dead and can be removed.
Loading history...
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

264
        $count = $count = @socket_select($select[self::CH_READ], $select[self::CH_WRITE], $select[self::CH_EXCEPT], /** @scrutinizer ignore-type */ $timeout, $usec);
Loading history...
265
        if ($count === false) {
266
            throw new Error($this->resource);
267
        }
268
        return (bool)$count;
269
    }
270
271
    /**
272
     * Polls for whether the socket can perform a non-blocking write.
273
     *
274
     * @see isReady()
275
     *
276
     * @return bool
277
     * @throws Error
278
     */
279
    public function isWritable () {
280
        return $this->isReady(self::CH_WRITE, 0);
281
    }
282
283
    /**
284
     * Stub. Called before the underlying resource is closed.
285
     *
286
     * @return void
287
     */
288
    protected function onClose () { }
289
290
    /**
291
     * Stub. Called before an I/O channel is shut down.
292
     *
293
     * @param int $channel `SocketInterface` channel constant.
294
     * @return void
295
     */
296
    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

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