AbstractSocket   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 334
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 1
dl 0
loc 334
rs 9.44
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
getType() 0 1 ?
A create() 0 6 2
A __construct() 0 13 5
A __destruct() 0 5 2
A await() 0 7 2
A awaitOutOfBand() 0 3 1
A awaitReadable() 0 3 1
A awaitWritable() 0 3 1
A getDomain() 0 3 1
A getId() 0 3 1
A getOption() 0 7 2
A getResource() 0 3 1
A getSockName() 0 6 2
A isOpen() 0 3 1
A isOutOfBand() 0 3 1
A isReadable() 0 3 1
A isReady() 0 10 2
A isWritable() 0 3 1
A setBlocking() 0 6 3
A setOption() 0 6 2
A setTimeout() 0 9 1
A shutdown() 0 6 2
A close() 0 6 2
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
     * @see 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($resource, ...$extra);
0 ignored issues
show
Unused Code introduced by
The call to AbstractSocket::__construct() has too many arguments starting with $extra.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

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 {@link 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 if it's open.
125
     *
126
     * @see https://php.net/socket_close
127
     *
128
     * @return $this
129
     */
130
    public function close () {
131
        if ($this->isOpen()) {
132
            socket_close($this->resource);
133
        }
134
        return $this;
135
    }
136
137
    /**
138
     * The `AF_*` address family constant.
139
     *
140
     * @return int
141
     */
142
    final public function getDomain (): int {
143
        return $this->getOption(39); // SO_DOMAIN is not exposed by PHP
144
    }
145
146
    /**
147
     * @return int
148
     */
149
    final public function getId (): int {
150
        return (int)$this->resource;
151
    }
152
153
    /**
154
     * Retrieves an option value.
155
     *
156
     * @see https://php.net/socket_get_option
157
     *
158
     * @param int $option `SO_*` option constant.
159
     * @return mixed The option's value. This is never `false`.
160
     * @throws SocketError
161
     */
162
    public function getOption (int $option) {
163
        $value = @socket_get_option($this->resource, SOL_SOCKET, $option);
164
        if ($value === false) {
165
            throw new SocketError($this->resource, SOCKET_EINVAL);
166
        }
167
        return $value;
168
    }
169
170
    /**
171
     * @return resource
172
     */
173
    final public function getResource () {
174
        return $this->resource;
175
    }
176
177
    /**
178
     * The local address and port, or Unix file path and port `0`.
179
     *
180
     * @see https://php.net/socket_getsockname
181
     *
182
     * @return array `[ 0 => address, 1 => port ]`
183
     * @throws SocketError
184
     */
185
    public function getSockName (): array {
186
        if (!@socket_getsockname($this->resource, $addr, $port)) {
187
            throw new SocketError($this->resource, SOCKET_EOPNOTSUPP);
188
        }
189
        return [$addr, $port];
190
    }
191
192
    /**
193
     * @return bool
194
     */
195
    public function isOpen (): bool {
196
        return is_resource($this->resource);
197
    }
198
199
    /**
200
     * Polls for whether the socket can perform a non-blocking out-of-band read.
201
     *
202
     * @see isReady()
203
     *
204
     * @return bool
205
     */
206
    final public function isOutOfBand (): bool {
207
        return $this->isReady(self::CH_EXCEPT);
208
    }
209
210
    /**
211
     * Polls for whether the socket can perform a non-blocking read.
212
     *
213
     * @see isReady()
214
     *
215
     * @return bool
216
     */
217
    final public function isReadable (): bool {
218
        return $this->isReady(self::CH_READ);
219
    }
220
221
    /**
222
     * Selects for channel availability.
223
     *
224
     * @see await()
225
     *
226
     * @param int $channel `SocketInterface` channel constant.
227
     * @param float|null $timeout Maximum seconds to block. `NULL` blocks forever. Defaults to a poll.
228
     * @return bool
229
     * @throws SocketError
230
     */
231
    public function isReady (int $channel, ?float $timeout = 0): bool {
232
        $rwe = [$channel => [$this->resource]];
233
        // core casts non-null timeout to int.
234
        // usec is ignored if timeout is null.
235
        $usec = (int)(fmod($timeout, 1) * 1000000);
236
        if (false === $count = @socket_select($rwe[0], $rwe[1], $rwe[2], $timeout, $usec)) {
237
            throw new SocketError($this->resource);
238
        }
239
        return (bool)$count;
240
    }
241
242
    /**
243
     * Polls for whether the socket can perform a non-blocking write.
244
     *
245
     * @see isReady()
246
     *
247
     * @return bool
248
     */
249
    final public function isWritable (): bool {
250
        return $this->isReady(self::CH_WRITE);
251
    }
252
253
    /**
254
     * Enables or disables blocking.
255
     *
256
     * **Note: PHP provides no way to retrieve a socket's blocking mode.**
257
     *
258
     * Sockets are always created in blocking mode.
259
     *
260
     * If you're using a reactor, keep the sockets in blocking mode.
261
     *
262
     * Non-blocking errors are thrown when performing ordinary operations, even if unrelated to those operations.
263
     * This is known as error slippage.
264
     * To test for and clear an existing non-blocking error, use `->getOption(SO_ERROR)`.
265
     *
266
     * @see https://php.net/socket_set_block
267
     * @see https://php.net/socket_set_nonblock
268
     *
269
     * @param bool $blocking Whether the socket should block.
270
     * @return $this
271
     * @throws SocketError
272
     */
273
    public function setBlocking (bool $blocking) {
274
        if ($blocking ? @socket_set_block($this->resource) : @socket_set_nonblock($this->resource)) {
275
            return $this;
276
        }
277
        throw new SocketError($this->resource); // reliable errno
278
    }
279
280
    /**
281
     * Sets an option on the underlying resource.
282
     *
283
     * @see https://php.net/socket_set_option
284
     *
285
     * @param int $option `SO_*` constant.
286
     * @param mixed $value
287
     * @return $this
288
     * @throws SocketError
289
     */
290
    public function setOption (int $option, $value) {
291
        if (!@socket_set_option($this->resource, SOL_SOCKET, $option, $value)) {
292
            throw new SocketError($this->resource, SOCKET_EINVAL);
293
        }
294
        return $this;
295
    }
296
297
    /**
298
     * Sets the I/O timeout length in seconds.
299
     *
300
     * @param float $timeout Zero means "no timeout" (block forever).
301
     * @return $this
302
     */
303
    public function setTimeout (float $timeout) {
304
        $tv = [
305
            'sec' => (int)$timeout,
306
            'usec' => (int)(fmod($timeout, 1) * 1000000)
307
        ];
308
        $this->setOption(SO_RCVTIMEO, $tv);
309
        $this->setOption(SO_SNDTIMEO, $tv);
310
        return $this;
311
    }
312
313
    /**
314
     * Shuts down I/O for a single channel.
315
     *
316
     * Shutting down is a formal handshake two peers can do before actually closing.
317
     * It's not required, but it can help assert I/O access logic.
318
     *
319
     * If writing is shut down, trying to send will throw `SOCKET_EPIPE`,
320
     * and the remote peer will read an empty string after receiving all pending data.
321
     *
322
     * If reading is shut down, trying to receive will return an empty string,
323
     * and the remote peer will get an `EPIPE` error if they try to send.
324
     *
325
     * Writing should be shut down first between two connected sockets.
326
     *
327
     * Selection always returns positive for a shut down channel,
328
     * and the remote peer will similarly have positive selection for the opposite channel.
329
     *
330
     * @see https://php.net/socket_shutdown
331
     *
332
     * @param int $channel `CH_READ` or `CH_WRITE`
333
     * @return $this
334
     * @throws SocketError
335
     */
336
    public function shutdown (int $channel) {
337
        if (!@socket_shutdown($this->resource, $channel)) {
338
            throw new SocketError($this->resource); // reliable errno
339
        }
340
        return $this;
341
    }
342
343
}