Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
NativeSocket
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
7 / 7
15
100.00% covered (success)
100.00%
1 / 1
 create
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 connect
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 close
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 read
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 send
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 __destruct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 assertCreated
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace AqwSocketClient\Sockets;
6
7use AqwSocketClient\Interfaces\SocketInterface;
8use Override;
9use Psl\Str\Byte;
10use RuntimeException;
11use Socket;
12
13/**
14 * Concrete socket implementation using PHP's native socket_* functions.
15 * @mago-ignore analyzer:possibly-null-argument
16 */
17final class NativeSocket implements SocketInterface
18{
19    private ?Socket $socket = null;
20
21    #[Override]
22    public function create(): void
23    {
24        $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
25        // @codeCoverageIgnoreStart
26        if ($socket === false) {
27            throw new RuntimeException('Failed to create socket: ' . socket_strerror(socket_last_error()));
28        }
29        // @codeCoverageIgnoreEnd
30
31        $this->socket = $socket;
32    }
33
34    #[Override]
35    public function connect(string $hostname, int $port): void
36    {
37        $this->assertCreated();
38
39        // @mago-expect lint:no-error-control-operator
40        if (!@socket_connect($this->socket, $hostname, $port)) {
41            $error = socket_strerror(socket_last_error($this->socket));
42            socket_close($this->socket);
43            $this->socket = null;
44
45            throw new RuntimeException('Failed to connect: ' . $error);
46        }
47    }
48
49    #[Override]
50    public function close(): void
51    {
52        if ($this->socket !== null) {
53            socket_close($this->socket);
54            $this->socket = null;
55        }
56    }
57
58    #[Override]
59    public function read(int $length): array
60    {
61        $this->assertCreated();
62        $chunk = '';
63        $attempts = 0;
64
65        while ($attempts < 100) {
66            $bytes = socket_recv($this->socket, $chunk, $length, MSG_DONTWAIT);
67
68            if ($bytes === false) {
69                $error = socket_last_error($this->socket);
70                if (in_array($error, [SOCKET_EAGAIN, SOCKET_EWOULDBLOCK], true)) {
71                    usleep(5000);
72                    $attempts++;
73                    continue;
74                }
75
76                // @codeCoverageIgnoreStart
77                throw new RuntimeException('Failed to receive data: ' . socket_strerror($error));
78
79                // @codeCoverageIgnoreEnd
80            }
81
82            return ['bytes' => $bytes, 'chunk' => $chunk];
83        }
84
85        return ['bytes' => 0, 'chunk' => ''];
86    }
87
88    #[Override]
89    public function send(string $data): int
90    {
91        $this->assertCreated();
92
93        $length = Byte\length($data);
94        $sent = socket_send($this->socket, $data, $length, 0);
95
96        // @codeCoverageIgnoreStart
97        if ($sent === false) {
98            throw new RuntimeException('Failed to send data: ' . socket_strerror(socket_last_error($this->socket)));
99        }
100        // @codeCoverageIgnoreEnd
101
102        return $sent;
103    }
104
105    public function __destruct()
106    {
107        $this->close();
108    }
109
110    private function assertCreated(): void
111    {
112        if ($this->socket === null) {
113            throw new RuntimeException('Socket has not been created yet.');
114        }
115    }
116}