Passed
Push — master ( f0b67b...ec9c11 )
by y
01:29
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 `SOCK_*` type constant for the class.
14
     *
15
     * @return int
16
     */
17
    abstract public static function getType (): int;
18
19
    /**
20
     * The underlying PHP resource.
21
     *
22
     * @var resource
23
     */
24
    protected $resource;
25
26
    /**
27
     * Creates an instance of the called class.
28
     *
29
     * @link https://php.net/socket_create
30
     *
31
     * @param int $domain `AF_*` constant.
32
     * @param array $extra Variadic constructor arguments.
33
     * @return static
34
     * @throws SocketError
35
     */
36
    public static function create (int $domain = AF_INET, ...$extra) {
37
        if (!$resource = @socket_create($domain, static::getType(), 0)) { // auto-protocol
38
            throw new SocketError; // reliable errno
39
        }
40
        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

40
        return new static(/** @scrutinizer ignore-type */ ...array_merge([$resource], $extra));
Loading history...
41
    }
42
43
    /**
44
     * Validates and sets the underlying socket resource.
45
     *
46
     * The resource must be open, of the correct type, and have no pending errors.
47
     *
48
     * @param resource $resource PHP socket resource.
49
     * @throws InvalidArgumentException Not a socket resource, or the socket is of the wrong type.
50
     * @throws SocketError Slippage of an existing error on the resource.
51
     */
52
    public function __construct ($resource) {
53
        if (!is_resource($resource) or get_resource_type($resource) !== 'Socket') {
54
            throw new InvalidArgumentException('Expected an open socket resource.', SOCKET_EBADF);
55
        }
56
        elseif (socket_get_option($resource, SOL_SOCKET, SO_TYPE) !== static::getType()) {
57
            throw new InvalidArgumentException('Invalid socket type for ' . static::class, SOCKET_ESOCKTNOSUPPORT);
58
        }
59
        elseif ($errno = SocketError::getLast($resource)) {
60
            // "File descriptor in bad state"
61
            throw new SocketError(SOCKET_EBADFD, 0, new SocketError($errno));
62
        }
63
        $this->resource = $resource;
64
    }
65
66
    /**
67
     * Closes the socket if it's open.
68
     *
69
     * @see close()
70
     */
71
    public function __destruct () {
72
        if ($this->isOpen()) {
73
            $this->close();
74
        }
75
    }
76
77
    /**
78
     * Blocks until the socket becomes available on a given channel.
79
     *
80
     * Throws if the underlying resource has a pending error (non-blocking sockets only).
81
     *
82
     * @see isReady()
83
     *
84
     * @param int $channel {@see SocketInterface} channel constant.
85
     * @return $this
86
     * @throws SocketError
87
     */
88
    public function await (int $channel) {
89
        $rwe = [$channel => [$this->resource]];
90
        if (!@socket_select($rwe[0], $rwe[1], $rwe[2], null)) {
91
            throw new SocketError($this->resource);
92
        }
93
        return $this;
94
    }
95
96
    /**
97
     * @see await()
98
     *
99
     * @return $this
100
     */
101
    final public function awaitOutOfBand () {
102
        return $this->await(self::CH_EXCEPT);
103
    }
104
105
    /**
106
     * @see await()
107
     *
108
     * @return $this
109
     */
110
    final public function awaitReadable () {
111
        return $this->await(self::CH_READ);
112
    }
113
114
    /**
115
     * @see await()
116
     *
117
     * @return $this
118
     */
119
    final public function awaitWritable () {
120
        return $this->await(self::CH_WRITE);
121
    }
122
123
    /**
124
     * Closes the underlying resource.
125
     *
126
     * @link https://php.net/socket_close
127
     *
128
     * @return $this
129
     */
130
    public function close () {
131
        socket_close($this->resource); // never errors
132
        return $this;
133
    }
134
135
    /**
136
     * The `AF_*` address family constant.
137
     *
138
     * @return int
139
     */
140
    final public function getDomain (): int {
141
        return $this->getOption(39); // SO_DOMAIN is not exposed by PHP
142
    }
143
144
    /**
145
     * @inheritDoc
146
     */
147
    final public function getId (): int {
148
        return (int)$this->resource;
149
    }
150
151
    /**
152
     * Retrieves an option value.
153
     *
154
     * @link https://php.net/socket_get_option
155
     *
156
     * @param int $option `SO_*` option constant.
157
     * @return mixed The option's value. This is never `false`.
158
     * @throws SocketError
159
     */
160
    public function getOption (int $option) {
161
        $value = @socket_get_option($this->resource, SOL_SOCKET, $option);
162
        if ($value === false) {
163
            throw new SocketError($this->resource, SOCKET_EINVAL);
164
        }
165
        return $value;
166
    }
167
168
    /**
169
     * @inheritDoc
170
     */
171
    final public function getResource () {
172
        return $this->resource;
173
    }
174
175
    /**
176
     * The local address and port, or Unix file path and port `0`.
177
     *
178
     * @link https://php.net/socket_getsockname
179
     *
180
     * @return array `[ 0 => address, 1 => port ]`
181
     * @throws SocketError
182
     */
183
    public function getSockName (): array {
184
        if (!@socket_getsockname($this->resource, $addr, $port)) {
185
            throw new SocketError($this->resource, SOCKET_EOPNOTSUPP);
186
        }
187
        return [$addr, $port];
188
    }
189
190
    /**
191
     * @inheritDoc
192
     */
193
    public function isOpen (): bool {
194
        return is_resource($this->resource);
195
    }
196
197
    /**
198
     * Polls for whether the socket can perform a non-blocking out-of-band read.
199
     *
200
     * @see isReady()
201
     *
202
     * @return bool
203
     */
204
    final public function isOutOfBand (): bool {
205
        return $this->isReady(self::CH_EXCEPT);
206
    }
207
208
    /**
209
     * Polls for whether the socket can perform a non-blocking read.
210
     *
211
     * @see isReady()
212
     *
213
     * @return bool
214
     */
215
    final public function isReadable (): bool {
216
        return $this->isReady(self::CH_READ);
217
    }
218
219
    /**
220
     * Selects for channel availability.
221
     *
222
     * @see await()
223
     *
224
     * @param int $channel `SocketInterface` channel constant.
225
     * @param float|null $timeout Maximum seconds to block. `NULL` blocks forever. Defaults to a poll.
226
     * @return bool
227
     * @throws SocketError
228
     */
229
    public function isReady (int $channel, ?float $timeout = 0): bool {
230
        $rwe = [$channel => [$this->resource]];
231
        // core casts non-null timeout to int.
232
        // usec is ignored if timeout is null.
233
        $usec = (int)(fmod($timeout, 1) * 1000000);
234
        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

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