• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

azjezz / psl / 22522197717

28 Feb 2026 02:01PM UTC coverage: 98.029% (+0.01%) from 98.016%
22522197717

Pull #587

github

azjezz
refactor(udp): split socket into two

Signed-off-by: azjezz <azjezz@protonmail.com>
Pull Request #587: refactor(udp): split socket into two

102 of 106 new or added lines in 7 files covered. (96.23%)

7510 of 7661 relevant lines covered (98.03%)

43.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.47
/src/Psl/UDP/ConnectedSocket.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Psl\UDP;
6

7
use Override;
8
use Psl\DateTime\Duration;
9
use Psl\IO;
10
use Psl\Network;
11

12
use function fclose;
13
use function is_resource;
14
use function stream_set_blocking;
15
use function stream_socket_get_name;
16
use function stream_socket_recvfrom;
17
use function stream_socket_sendto;
18

19
use const STREAM_PEEK;
20

21
/**
22
 * A connected UDP socket for communicating with a single peer.
23
 *
24
 * Obtained via {@see Socket::connect()} or {@see connect()}.
25
 */
26
final class ConnectedSocket implements Network\SocketInterface, IO\StreamHandleInterface
27
{
28
    /**
29
     * @var resource|closed-resource|null
30
     */
31
    private mixed $stream;
32

33
    /**
34
     * @internal Use {@see Socket::connect()} or {@see connect()} to obtain a ConnectedSocket.
35
     *
36
     * @param resource $stream
37
     */
38
    public function __construct(
39
        mixed $stream,
40
        private readonly Network\Address $peerAddress,
41
    ) {
42
        $this->stream = $stream;
20✔
43
        stream_set_blocking($stream, false);
20✔
44
    }
45

46
    /**
47
     * Send a datagram to the connected peer.
48
     *
49
     * @return int<0, max> Number of bytes sent.
50
     *
51
     * @throws Network\Exception\RuntimeException If the send fails.
52
     * @throws Network\Exception\InvalidArgumentException If the datagram exceeds the maximum size.
53
     * @throws IO\Exception\TimeoutException If the operation times out.
54
     * @throws IO\Exception\AlreadyClosedException If the socket has already been closed.
55
     */
56
    public function send(string $data, null|Duration $timeout = null): int
57
    {
58
        Internal\validate_payload_size($data);
7✔
59
        $stream = $this->getResource();
6✔
60

61
        if ($timeout !== null) {
5✔
62
            Internal\wait_writable($stream, $timeout);
1✔
63
        }
64

65
        $result = @stream_socket_sendto($stream, $data);
5✔
66
        if ($result === false || $result === -1) {
5✔
NEW
67
            throw new Network\Exception\RuntimeException('Failed to send UDP datagram.');
×
68
        }
69

70
        /** @var int<0, max> */
71
        return $result;
5✔
72
    }
73

74
    /**
75
     * Receive a datagram from the connected peer.
76
     *
77
     * @param positive-int $max_bytes
78
     *
79
     * @throws Network\Exception\RuntimeException If the receive fails.
80
     * @throws IO\Exception\TimeoutException If the operation times out.
81
     * @throws IO\Exception\AlreadyClosedException If the socket has already been closed.
82
     */
83
    public function receive(int $max_bytes, null|Duration $timeout = null): string
84
    {
85
        $stream = $this->getResource();
5✔
86

87
        Internal\await_readable($stream, $timeout);
4✔
88

89
        $data = @stream_socket_recvfrom($stream, $max_bytes, 0);
3✔
90
        if ($data === false) {
3✔
NEW
91
            throw new Network\Exception\RuntimeException('Failed to receive UDP datagram.');
×
92
        }
93

94
        return $data;
3✔
95
    }
96

97
    /**
98
     * Peek at an incoming datagram without consuming it.
99
     *
100
     * @param positive-int $max_bytes
101
     *
102
     * @throws Network\Exception\RuntimeException If the peek fails.
103
     * @throws IO\Exception\TimeoutException If the operation times out.
104
     * @throws IO\Exception\AlreadyClosedException If the socket has already been closed.
105
     */
106
    public function peek(int $max_bytes, null|Duration $timeout = null): string
107
    {
108
        $stream = $this->getResource();
3✔
109

110
        Internal\await_readable($stream, $timeout);
2✔
111

112
        $data = @stream_socket_recvfrom($stream, $max_bytes, STREAM_PEEK);
1✔
113
        if ($data === false) {
1✔
NEW
114
            throw new Network\Exception\RuntimeException('Failed to peek UDP datagram.');
×
115
        }
116

117
        return $data;
1✔
118
    }
119

120
    /**
121
     * Get the local address this socket is bound to.
122
     *
123
     * @throws Network\Exception\RuntimeException If unable to retrieve local address.
124
     * @throws IO\Exception\AlreadyClosedException If the socket has already been closed.
125
     */
126
    #[Override]
127
    public function getLocalAddress(): Network\Address
128
    {
129
        $stream = $this->getResource();
2✔
130
        $name = @stream_socket_get_name($stream, false);
1✔
131
        if ($name === false) {
1✔
NEW
132
            throw new Network\Exception\RuntimeException('Failed to get local address.');
×
133
        }
134

135
        return Internal\parse_address($name);
1✔
136
    }
137

138
    /**
139
     * Get the peer address this socket is connected to.
140
     */
141
    public function getPeerAddress(): Network\Address
142
    {
143
        return $this->peerAddress;
3✔
144
    }
145

146
    /**
147
     * @return resource|object|null
148
     */
149
    #[Override]
150
    public function getStream(): mixed
151
    {
152
        if (!is_resource($this->stream)) {
3✔
153
            return null;
2✔
154
        }
155

156
        return $this->stream;
1✔
157
    }
158

159
    #[Override]
160
    public function close(): void
161
    {
162
        if (is_resource($this->stream)) {
20✔
163
            fclose($this->stream);
20✔
164
        }
165

166
        $this->stream = null;
20✔
167
    }
168

169
    public function __destruct()
170
    {
171
        $this->close();
20✔
172
    }
173

174
    /**
175
     * @return resource
176
     *
177
     * @throws IO\Exception\AlreadyClosedException If the socket has already been closed.
178
     */
179
    private function getResource(): mixed
180
    {
181
        if (!is_resource($this->stream)) {
12✔
182
            throw new IO\Exception\AlreadyClosedException('UDP socket has already been closed.');
4✔
183
        }
184

185
        return $this->stream;
8✔
186
    }
187
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc