Passed
Push — master ( a91d54...875978 )
by y
02:08
created

AbstractSocket::select()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 2
eloc 14
c 3
b 0
f 0
nc 2
nop 4
dl 0
loc 18
rs 9.7998
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 SocketError
43
     */
44
    public static function create ($domain = AF_INET, ...$extra) {
45
        if (!$resource = @socket_create($domain, static::getType(), 0)) { // auto-protocol
46
            throw new SocketError; // 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
     * Validates and sets the underlying socket resource.
53
     *
54
     * The resource must be open, of the correct type, and have no pending errors.
55
     *
56
     * @param resource $resource PHP socket resource.
57
     * @throws InvalidArgumentException Not a socket resource, or the socket is of the wrong type.
58
     * @throws SocketError (`SOCKET_EBADFD`) There was a pending error on the socket.
59
     * The preexisting error code is set as the exception's extra data.
60
     */
61
    public function __construct ($resource) {
62
        if (!is_resource($resource) or get_resource_type($resource) !== 'Socket') {
63
            throw new InvalidArgumentException('Expected an open socket resource.', SOCKET_EBADF);
64
        }
65
        elseif (socket_get_option($resource, SOL_SOCKET, SO_TYPE) !== static::getType()) {
66
            throw new InvalidArgumentException('The given socket resource is of the wrong type for ' . get_class(), SOCKET_ESOCKTNOSUPPORT);
67
        }
68
        elseif ($errno = SocketError::getLast($resource)) {
69
            $error = new SocketError(SOCKET_EBADFD);
70
            $error->setExtra($errno);
71
            throw $error;
72
        }
73
        $this->resource = $resource;
74
        $this->domain = $this->getOption(self::SO_DOMAIN);
75
        $this->protocol = $this->getOption(self::SO_PROTOCOL);
76
    }
77
78
    /**
79
     * Closes the socket if it's open.
80
     */
81
    public function __destruct () {
82
        if ($this->isOpen()) {
83
            $this->close();
84
        }
85
    }
86
87
    /**
88
     * Blocks until the socket becomes available on a given channel.
89
     *
90
     * Throws if the underlying resource has a pending error (non-blocking sockets only).
91
     *
92
     * @see isReady()
93
     *
94
     * @param int $channel `SocketInterface` channel constant.
95
     * @return $this
96
     * @throws SocketError
97
     */
98
    public function await ($channel) {
99
        $rwe = [$channel => [$this->resource]];
100
        if (!@socket_select($rwe[0], $rwe[1], $rwe[2], null)) {
101
            throw new SocketError($this->resource);
102
        }
103
        return $this;
104
    }
105
106
    /**
107
     * @see await()
108
     *
109
     * @return $this
110
     */
111
    final public function awaitOutOfBand () {
112
        return $this->await(self::CH_EXCEPT);
113
    }
114
115
    /**
116
     * @see await()
117
     *
118
     * @return $this
119
     */
120
    final public function awaitReadable () {
121
        return $this->await(self::CH_READ);
122
    }
123
124
    /**
125
     * @see await()
126
     *
127
     * @return $this
128
     */
129
    final public function awaitWritable () {
130
        return $this->await(self::CH_WRITE);
131
    }
132
133
    /**
134
     * Closes the socket if it's open.
135
     *
136
     * @link https://php.net/socket_close
137
     *
138
     * @return $this
139
     */
140
    public function close () {
141
        socket_close($this->resource); // never errors
142
        return $this;
143
    }
144
145
    /**
146
     * @return int
147
     */
148
    final public function getDomain (): int {
149
        return $this->domain;
150
    }
151
152
    /**
153
     * @return int
154
     */
155
    final public function getId (): int {
156
        return (int)$this->resource;
157
    }
158
159
    /**
160
     * Retrieves a socket-level option.
161
     *
162
     * @link https://php.net/socket_get_option
163
     *
164
     * @param int $option `SO_*` option constant.
165
     * @throws SocketError
166
     * @return mixed The option's value. This is never `false`.
167
     */
168
    public function getOption ($option) {
169
        $value = @socket_get_option($this->resource, SOL_SOCKET, $option);
170
        if ($value === false) {
171
            throw new SocketError($this->resource, SOCKET_EINVAL);
172
        }
173
        return $value;
174
    }
175
176
    /**
177
     * @return int
178
     */
179
    final public function getProtocol (): int {
180
        return $this->protocol;
181
    }
182
183
    /**
184
     * @return resource
185
     */
186
    final public function getResource () {
187
        return $this->resource;
188
    }
189
190
    /**
191
     * Returns the local address and port, or Unix file path and port `0`.
192
     *
193
     * @link https://php.net/socket_getsockname
194
     *
195
     * @throws SocketError
196
     * @return array String address/file and integer port at indices `0` and `1`.
197
     */
198
    public function getSockName () {
199
        if (!@socket_getsockname($this->resource, $addr, $port)) {
200
            throw new SocketError($this->resource, SOCKET_EOPNOTSUPP);
201
        }
202
        return [$addr, $port];
203
    }
204
205
    /**
206
     * Whether the underlying resource is open.
207
     *
208
     * @return bool
209
     */
210
    public function isOpen (): bool {
211
        return is_resource($this->resource);
212
    }
213
214
    /**
215
     * Polls for whether the socket can perform a non-blocking out-of-band read.
216
     *
217
     * @see isReady()
218
     *
219
     * @return bool
220
     * @throws SocketError
221
     */
222
    public function isOutOfBand () {
223
        return $this->isReady(self::CH_EXCEPT);
224
    }
225
226
    /**
227
     * Polls for whether the socket can perform a non-blocking read.
228
     *
229
     * @see isReady()
230
     *
231
     * @return bool
232
     * @throws SocketError
233
     */
234
    final public function isReadable () {
235
        return $this->isReady(self::CH_READ);
236
    }
237
238
    /**
239
     * Selects for channel availability.
240
     *
241
     * @see await()
242
     *
243
     * @param int $channel `SocketInterface` channel constant.
244
     * @param float|null $timeout Maximum seconds to block. `NULL` blocks forever. Defaults to a poll.
245
     * @return bool
246
     * @throws SocketError
247
     */
248
    public function isReady ($channel, $timeout = 0.0): bool {
249
        $rwe = [$channel => [$this->resource]];
250
        // core casts non-null timeout to int.
251
        // usec is ignored if timeout is null.
252
        $usec = (int)(fmod($timeout, 1) * 1000000);
253
        if (false === $count = @socket_select($rwe[0], $rwe[1], $rwe[2], $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

253
        if (false === $count = @socket_select($rwe[0], $rwe[1], $rwe[2], /** @scrutinizer ignore-type */ $timeout, $usec)) {
Loading history...
254
            throw new SocketError($this->resource);
255
        }
256
        return (bool)$count;
257
    }
258
259
    /**
260
     * Polls for whether the socket can perform a non-blocking write.
261
     *
262
     * @see isReady()
263
     *
264
     * @return bool
265
     * @throws SocketError
266
     */
267
    final public function isWritable () {
268
        return $this->isReady(self::CH_WRITE);
269
    }
270
271
    /**
272
     * Enables or disables blocking.
273
     *
274
     * **Note: PHP provides no way to retrieve a socket's blocking mode.**
275
     *
276
     * Sockets are always created in blocking mode.
277
     *
278
     * Non-blocking errors are thrown when performing ordinary operations, even if unrelated to those operations.
279
     * This is known as error slippage.
280
     * To test for and clear an existing non-blocking error, use `->getOption(SO_ERROR)`.
281
     *
282
     * @link https://php.net/socket_set_block
283
     * @link https://php.net/socket_set_nonblock
284
     *
285
     * @param bool $blocking Whether the socket should block.
286
     * @throws SocketError
287
     * @return $this
288
     */
289
    public function setBlocking ($blocking) {
290
        if ($blocking) {
291
            $success = @socket_set_block($this->resource);
292
        }
293
        else {
294
            $success = @socket_set_nonblock($this->resource);
295
        }
296
        if (!$success) {
297
            throw new SocketError($this->resource); // reliable errno
298
        }
299
        return $this;
300
    }
301
302
    /**
303
     * Sets an option on the underlying resource.
304
     *
305
     * @link https://php.net/socket_set_option
306
     *
307
     * @param int $option `SO_*` constant.
308
     * @param mixed $value
309
     * @throws SocketError
310
     * @return $this
311
     */
312
    public function setOption ($option, $value) {
313
        if (!@socket_set_option($this->resource, SOL_SOCKET, $option, $value)) {
314
            throw new SocketError($this->resource, SOCKET_EINVAL);
315
        }
316
        return $this;
317
    }
318
319
    /**
320
     * Sets the I/O timeout length in seconds.
321
     *
322
     * @param float $timeout Zero means "no timeout" (block forever).
323
     * @return $this
324
     * @throws SocketError
325
     */
326
    public function setTimeout ($timeout) {
327
        $tv = [
328
            'sec' => (int)$timeout,
329
            'usec' => (int)(fmod($timeout, 1) * 1000000)
330
        ];
331
        $this->setOption(SO_RCVTIMEO, $tv);
332
        $this->setOption(SO_SNDTIMEO, $tv);
333
        return $this;
334
    }
335
336
    /**
337
     * Shuts down I/O.
338
     *
339
     * Shutting down is a formal handshake two peers can do before actually closing.
340
     * It's not required, but it can help assert I/O access logic.
341
     *
342
     * If writing is shut down, trying to send will throw `SOCKET_EPIPE`,
343
     * and the remote peer will read an empty string after receiving all pending data.
344
     *
345
     * If reading is shut down, trying to receive will return an empty string,
346
     * and the remote peer will get an `EPIPE` error if they try to send.
347
     *
348
     * Writing should be shut down first between two connected sockets.
349
     *
350
     * Selection always returns positive for a shut down channel,
351
     * and the remote peer will similarly have positive selection for the opposite channel.
352
     *
353
     * @link https://php.net/socket_shutdown
354
     *
355
     * @param int $channels `CH_READ | CH_WRITE`
356
     * @throws SocketError
357
     * @return $this
358
     */
359
    public function shutdown (int $channels = self::CH_READ | self::CH_WRITE) {
360
        if (!@socket_shutdown($this->resource, $channels)) {
361
            throw new SocketError($this->resource); // reliable errno
362
        }
363
        return $this;
364
    }
365
366
}