WebSocketClient::getState()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Helix\Socket\WebSocket;
4
5
use Helix\Socket\ReactiveInterface;
6
use Helix\Socket\StreamClient;
7
use JsonException;
8
use Throwable;
9
10
/**
11
 * Wraps a WebSocket peer.
12
 *
13
 * @see https://tools.ietf.org/html/rfc6455
14
 */
15
class WebSocketClient extends StreamClient implements ReactiveInterface {
16
17
    /**
18
     * The peer has connected but hasn't negotiated a session yet.
19
     */
20
    const STATE_HANDSHAKE = 0;
21
22
    /**
23
     * The session is active and the client can perform frame I/O with the peer.
24
     */
25
    const STATE_OK = 1;
26
27
    /**
28
     * The peer has disconnected.
29
     */
30
    const STATE_CLOSED = 2;
31
32
    /**
33
     * @var FrameHandler
34
     */
35
    protected $frameHandler;
36
37
    /**
38
     * @var Handshake
39
     */
40
    protected $handshake;
41
42
    /**
43
     * @var WebSocketServer
44
     */
45
    protected $server;
46
47
    /**
48
     * @var int
49
     */
50
    protected $state = self::STATE_HANDSHAKE;
51
52
    /**
53
     * @param resource $resource
54
     * @param WebSocketServer $server
55
     */
56
    public function __construct ($resource, WebSocketServer $server) {
57
        parent::__construct($resource);
58
        $this->server = $server;
59
        $this->handshake = new Handshake($this);
60
        $this->frameHandler = new FrameHandler($this);
61
    }
62
63
    /**
64
     * Closes, optionally with a code and reason sent to the peer.
65
     *
66
     * https://tools.ietf.org/html/rfc6455#section-5.5.1
67
     * > The application MUST NOT send any more data frames after sending a
68
     * > Close frame.
69
     * >
70
     * > After both sending and receiving a Close message, an endpoint
71
     * > considers the WebSocket connection closed and MUST close the
72
     * > underlying TCP connection.
73
     *
74
     * https://tools.ietf.org/html/rfc6455#section-7.4.2
75
     * > Status codes in the range 0-999 are not used.
76
     *
77
     * @param int|null $code Sent to the peer if >= 1000
78
     * @param string $reason Sent to the peer, if code is >= 1000
79
     * @return $this
80
     */
81
    public function close (int $code = null, string $reason = '') {
82
        try {
83
            if ($code >= 1000 and $this->isOk()) {
84
                $this->frameHandler->writeClose($code, $reason);
85
            }
86
        }
87
        finally {
88
            $this->server->remove($this);
89
            parent::close();
90
            $this->state = self::STATE_CLOSED;
91
        }
92
        return $this;
93
    }
94
95
    /**
96
     * @return FrameHandler
97
     */
98
    public function getFrameHandler (): FrameHandler {
99
        return $this->frameHandler;
100
    }
101
102
    /**
103
     * @return WebSocketServer
104
     */
105
    public function getServer (): WebSocketServer {
106
        return $this->server;
107
    }
108
109
    /**
110
     * @return int
111
     */
112
    public function getState (): int {
113
        return $this->state;
114
    }
115
116
    final public function isNegotiating (): bool {
117
        return $this->state === self::STATE_HANDSHAKE;
118
    }
119
120
    /**
121
     * @return bool
122
     */
123
    final public function isOk (): bool {
124
        return $this->state === self::STATE_OK;
125
    }
126
127
    /**
128
     * Called when a complete `BINARY` payload is received from the peer.
129
     *
130
     * Throws by default.
131
     *
132
     * @param string $binary
133
     * @throws WebSocketError
134
     */
135
    public function onBinary (string $binary): void {
136
        unset($binary);
137
        throw new WebSocketError(Frame::CLOSE_UNHANDLED_DATA, "I don't handle binary data.");
138
    }
139
140
    /**
141
     * Called when a `CLOSE` frame is received from the peer.
142
     *
143
     * @param int $code
144
     * @param string $reason
145
     */
146
    public function onClose (int $code, string $reason): void {
147
        unset($code, $reason);
148
        $this->close();
149
    }
150
151
    /**
152
     * WebSockets do not use the out-of-band channel.
153
     *
154
     * The RFC says the connection must be dropped if any unsupported activity occurs.
155
     *
156
     * Closes the connection with a protocol-error frame.
157
     */
158
    final public function onOutOfBand (): void {
159
        $this->close(Frame::CLOSE_PROTOCOL_ERROR, "Received out-of-band data.");
160
    }
161
162
    /**
163
     * Called when a `PING` is received from the peer.
164
     *
165
     * Automatically PONGs back the payload back by default.
166
     *
167
     * @param string $message
168
     */
169
    public function onPing (string $message): void {
170
        $this->frameHandler->writePong($message);
171
    }
172
173
    /**
174
     * Called when a `PONG` is received from the peer.
175
     *
176
     * Does nothing by default.
177
     *
178
     * @param string $message
179
     */
180
    public function onPong (string $message): void {
0 ignored issues
show
Unused Code introduced by
The parameter $message is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
181
        // stub
182
    }
183
184
    /**
185
     * Delegates the read-channel to handlers.
186
     *
187
     * @throws WebSocketError
188
     * @throws Throwable
189
     */
190
    public function onReadable (): void {
191
        try {
192
            if ($this->isNegotiating()) {
193
                if ($this->handshake->onReadable()) {
194
                    $this->state = self::STATE_OK;
195
                    $this->onStateOk();
196
                }
197
            }
198
            elseif ($this->isOk()) {
199
                $this->frameHandler->onReadable();
200
            }
201
        }
202
        catch (WebSocketError $e) {
203
            $this->close($e->getCode(), $e->getMessage());
204
            throw $e;
205
        }
206
        catch (Throwable $e) {
207
            $this->close(Frame::CLOSE_INTERNAL_ERROR);
208
            throw $e;
209
        }
210
    }
211
212
    /**
213
     * Called when the initial connection handshake succeeds and frame I/O can occur.
214
     *
215
     * Does nothing by default.
216
     *
217
     * If you have negotiated an extension during {@link Handshake},
218
     * claim the RSV bits here via {@link FrameReader::setRsv()}
219
     */
220
    protected function onStateOk (): void {
221
        // stub
222
    }
223
224
    /**
225
     * Called when a complete `TEXT` payload is received from the peer.
226
     *
227
     * Throws by default.
228
     *
229
     * @param string $text
230
     * @throws WebSocketError
231
     */
232
    public function onText (string $text): void {
233
        unset($text);
234
        throw new WebSocketError(Frame::CLOSE_UNHANDLED_DATA, "I don't handle text.");
235
    }
236
237
    /**
238
     * Forwards to the {@link FrameHandler}
239
     *
240
     * @param string $binary
241
     */
242
    public function writeBinary (string $binary): void {
243
        $this->frameHandler->writeBinary($binary);
244
    }
245
246
    /**
247
     * JSON-encodes and sends as `TEXT`.
248
     *
249
     * @param mixed $data
250
     * @throws JsonException
251
     */
252
    public function writeJSON ($data): void {
253
        $this->frameHandler->writeText(json_encode($data, JSON_THROW_ON_ERROR));
254
    }
255
256
    /**
257
     * Forwards to the {@link FrameHandler}
258
     *
259
     * @param string $text
260
     */
261
    public function writeText (string $text): void {
262
        $this->frameHandler->writeText($text);
263
    }
264
265
}