Skip to content

Commit b543883

Browse files
committed
Opcode Registry
1 parent d7d7278 commit b543883

15 files changed

Lines changed: 306 additions & 44 deletions

docs/Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
99
### `3.8.0`
1010

11+
* Configurable opcode/class mapping (@sirn-se)
1112
* Dispach onDisconnect on remote connection close (@srebb, @sirn-se)
1213
* Exceptions: ControlInterface on CloseException, ReconnectException (@sirn-se)
1314
* Exceptions: HandlerLevelInterface on BadUriException, ClientException, RunnerException, ServerException (@sirn-se)

docs/Configuration.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,33 @@ $frameSize = $server->getFrameSize();
145145
$server->setFrameSize($frameSize);
146146
```
147147

148+
### Opcode registry
149+
150+
```
151+
type: WebSocket\Message\OpcodeRegistry
152+
default: Phrity\Net\OpcodeRegistry // Default mapping
153+
```
154+
155+
By default, opcodes are mapped to built-in classes (text, binary, close, ping, pong).
156+
With the OpcodeRegistry, the library can be configured to replace or add additional opcode imlementations.
157+
158+
```php
159+
// Configuration instance
160+
$configuration = new WebSocket\Configuration(opcodeRegistry: $opcodeRegistry);
161+
$configuration->setOpcodeRegistry($opcodeRegistry);
162+
$opcodeRegistry = $configuration->getOpcodeRegistry();
163+
```
164+
165+
The `register(int $opcode, string $classname)` method will replace or add mapping.
166+
- `$opcode` must be integer in range 1-15
167+
- `$classname` must be class-string for class that extend `WebSocket\Message\Message`
168+
169+
```php
170+
$opcodeRegistry = $configuration->getOpcodeRegistry();
171+
$opcodeRegistry->register(1, 'MyTextMessage'); // Will replace Text for opcode=1
172+
$opcodeRegistry->register(3, 'MyCustomMessage'); // Will add support for opcode=3
173+
```
174+
148175
### Persistent connection (Client only)
149176

150177
```

src/Configuration.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
NullLogger,
1616
};
1717
use Stringable;
18+
use WebSocket\Message\OpcodeRegistry;
1819
use WebSocket\Trait\StringableTrait;
1920

2021
/**
@@ -34,6 +35,7 @@ class Configuration implements LoggerAwareInterface, Stringable
3435
private bool $persistent;
3536
/** @var int<1, max>|null $maxConnections */
3637
private int|null $maxConnections;
38+
private OpcodeRegistry $opcodeRegistry;
3739

3840

3941
/* ---------- Magic methods ------------------------------------------------------------------------------------ */
@@ -45,6 +47,7 @@ class Configuration implements LoggerAwareInterface, Stringable
4547
* @param int<1, max> $frameSize
4648
* @param bool $persistent
4749
* @param int<1, max>|null $maxConnections
50+
* @param OpcodeRegistry $opcodeRegistry
4851
*/
4952
public function __construct(
5053
LoggerInterface|null $logger = null,
@@ -53,13 +56,15 @@ public function __construct(
5356
int|null $frameSize = null,
5457
bool|null $persistent = null,
5558
int|null $maxConnections = null,
59+
OpcodeRegistry|null $opcodeRegistry = null,
5660
) {
5761
$this->setLogger($logger ?? new NullLogger());
5862
$this->setContext($context ?? new Context());
5963
$this->setTimeout($timeout ?? 60);
6064
$this->setFrameSize($frameSize ?? 4096);
6165
$this->setPersistent($persistent ?? false);
6266
$this->setMaxConnections($maxConnections);
67+
$this->setOpcodeRegistry($opcodeRegistry ?? new OpcodeRegistry());
6368
}
6469

6570
public function __toString(): string
@@ -151,6 +156,24 @@ public function setFrameSize(int $frameSize): void
151156
$this->frameSize = $frameSize;
152157
}
153158

159+
/* ---------- OpcodeRegistry methods --------------------------------------------------------------------------- */
160+
161+
/**
162+
* @return OpcodeRegistry
163+
*/
164+
public function getOpcodeRegistry(): OpcodeRegistry
165+
{
166+
return $this->opcodeRegistry;
167+
}
168+
169+
/**
170+
* @param OpcodeRegistry $opcodeRegistry
171+
*/
172+
public function setOpcodeRegistry(OpcodeRegistry $opcodeRegistry): void
173+
{
174+
$this->opcodeRegistry = $opcodeRegistry;
175+
}
176+
154177

155178
/* ---------- Persistent methods (Client only) ----------------------------------------------------------------- */
156179

src/Connection.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,12 @@ public function __construct(
8989
Configuration|null $configuration = null,
9090
) {
9191
$this->stream = $stream;
92+
$this->initConfiguration($configuration);
9293
$this->httpHandler = new HttpHandler($this->stream, $ssl, $httpFactory);
93-
$this->messageHandler = new MessageHandler(new FrameHandler($this->stream, $pushMasked, $pullMaskedRequired));
94+
$this->messageHandler = new MessageHandler(
95+
new FrameHandler($this->stream, $pushMasked, $pullMaskedRequired),
96+
$this->configuration
97+
);
9498
$this->middlewareHandler = new MiddlewareHandler($this->messageHandler, $this->httpHandler);
9599
$this->localName = $this->stream->getLocalName() ?? '<unknown>';
96100
$this->remoteName = $this->stream->getRemoteName() ?? '<unknown>';
@@ -99,7 +103,6 @@ public function __construct(
99103
$this->getIdentityPart($this->localName),
100104
$this->getIdentityPart($this->remoteName),
101105
);
102-
$this->initConfiguration($configuration);
103106
$this->stream->setTimeout($this->configuration->getTimeout());
104107
}
105108

src/Frame/Frame.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ class Frame implements Stringable
1818
{
1919
use StringableTrait;
2020

21-
private string $opcode;
21+
/** @var int<0, 15> $opcode */
22+
private int $opcode;
2223
private string $payload;
2324
private bool $final;
2425
private bool $rsv1;
2526
private bool $rsv2;
2627
private bool $rsv3;
2728

29+
/**
30+
* @param int<0, 15> $opcode
31+
*/
2832
public function __construct(
29-
string $opcode,
33+
int $opcode,
3034
string $payload,
3135
bool $final,
3236
bool $rsv1 = false,
@@ -68,10 +72,13 @@ public function getRsv3(): bool
6872

6973
public function isContinuation(): bool
7074
{
71-
return $this->opcode === 'continuation';
75+
return $this->opcode === 0;
7276
}
7377

74-
public function getOpcode(): string
78+
/**
79+
* @return int<0, 15>
80+
*/
81+
public function getOpcode(): int
7582
{
7683
return $this->opcode;
7784
}

src/Frame/FrameHandler.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use WebSocket\Exception\CloseException;
1919
use WebSocket\Trait\{
2020
ConfigurationTrait,
21-
OpcodeTrait,
2221
StringableTrait
2322
};
2423

@@ -29,7 +28,6 @@
2928
class FrameHandler implements LoggerAwareInterface, Stringable
3029
{
3130
use ConfigurationTrait;
32-
use OpcodeTrait;
3331
use StringableTrait;
3432

3533
private const SCOPE = 'frame-handler';
@@ -75,9 +73,7 @@ public function pull(): Frame
7573
$rsv3 = (bool)($byte1 & 0b00010000);
7674

7775
// Parse opcode
78-
$opcodeInt = $byte1 & 0b00001111;
79-
$opcodeInts = array_flip(self::$opcodes);
80-
$opcode = array_key_exists($opcodeInt, $opcodeInts) ? $opcodeInts[$opcodeInt] : strval($opcodeInt);
76+
$opcode = $byte1 & 0b00001111;
8177

8278
// Masking bit
8379
$masked = (bool)($byte2 & 0b10000000);
@@ -144,7 +140,7 @@ public function push(Frame $frame): int
144140
$byte1 |= $frame->getRsv1() ? 0b01000000 : 0b00000000; // RSV1 bit.
145141
$byte1 |= $frame->getRsv2() ? 0b00100000 : 0b00000000; // RSV2 bit.
146142
$byte1 |= $frame->getRsv3() ? 0b00010000 : 0b00000000; // RSV3 bit.
147-
$byte1 |= self::$opcodes[$frame->getOpcode()]; // Set opcode.
143+
$byte1 |= $frame->getOpcode(); // Set opcode.
148144
$data .= pack('C', $byte1);
149145

150146
$byte2 = $this->pushMasked ? 0b10000000 : 0b00000000; // Masking bit marker.

src/Message/Message.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,20 @@ public function setCompress(bool $compress): void
8989
/**
9090
* Split messages into frames
9191
* @param int<1, max> $frameSize
92+
* @param OpcodeRegistry|null $opcodeRegistry
9293
* @return array<Frame>
9394
*/
94-
public function getFrames(int $frameSize = 4096): array
95+
public function getFrames(int $frameSize = 4096, OpcodeRegistry|null $opcodeRegistry = null): array
9596
{
97+
$opcodeRegistry = $opcodeRegistry ?? new OpcodeRegistry();
9698
$frames = [];
9799
$split = str_split($this->getPayload(), $frameSize);
98100
if (empty($split)) {
99101
$split = [''];
100102
}
101103
foreach ($split as $i => $payload) {
102104
$frames[] = new Frame(
103-
$i === 0 ? $this->opcode : 'continuation',
105+
$i === 0 ? $opcodeRegistry->getOpcode(static::class) : 0,
104106
$payload,
105107
$i === array_key_last($split)
106108
);

src/Message/MessageHandler.php

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
};
1414
use Stringable;
1515
use WebSocket\Configuration;
16-
use WebSocket\Exception\BadOpcodeException;
1716
use WebSocket\Frame\{
1817
Frame,
1918
FrameHandler,
2019
};
2120
use WebSocket\Trait\{
2221
ConfigurationTrait,
22+
OpcodeTrait,
2323
StringableTrait,
2424
};
2525

@@ -30,6 +30,7 @@
3030
class MessageHandler implements LoggerAwareInterface, Stringable
3131
{
3232
use ConfigurationTrait;
33+
use OpcodeTrait;
3334
use StringableTrait;
3435

3536
private const DEFAULT_SIZE = 4096;
@@ -65,7 +66,7 @@ public function setLogger(LoggerInterface $logger): void
6566
*/
6667
public function push(Message $message, int $size = self::DEFAULT_SIZE): Message
6768
{
68-
$frames = $message->getFrames($size);
69+
$frames = $message->getFrames($size, $this->configuration->getOpcodeRegistry());
6970
foreach ($frames as $frame) {
7071
$this->frameHandler->push($frame);
7172
}
@@ -100,19 +101,13 @@ public function pull(): Message
100101

101102
/**
102103
* @param non-empty-array<Frame> $frames
103-
* @throws BadOpcodeException
104104
*/
105105
private function createMessage(array $frames): Message
106106
{
107-
$opcode = $frames[0]->getOpcode() ?? null;
108-
$message = match ($opcode) {
109-
'text' => new Text(),
110-
'binary' => new Binary(),
111-
'ping' => new Ping(),
112-
'pong' => new Pong(),
113-
'close' => new Close(),
114-
default => throw new BadOpcodeException("Invalid opcode '{$opcode}' provided"),
115-
};
107+
/** @var int<1, 15> $opcode */
108+
$opcode = $frames[0]->getOpcode();
109+
$message = $this->configuration->getOpcodeRegistry()->createMessage($opcode);
110+
116111
$message->setPayload(array_reduce($frames, function (string $carry, Frame $item) {
117112
return $carry . $item->getPayload();
118113
}, ''));

src/Message/OpcodeRegistry.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
/**
4+
* Copyright (C) 2014-2026 Textalk and contributors.
5+
* This file is part of Websocket PHP and is free software under the ISC License.
6+
*/
7+
8+
namespace WebSocket\Message;
9+
10+
use DomainException;
11+
use InvalidArgumentException;
12+
use RangeException;
13+
use WebSocket\Exception\BadOpcodeException;
14+
15+
/**
16+
* WebSocket\Message\OpcodeRegistry class.
17+
* Mapping opcode <-> Message implementation class
18+
*/
19+
class OpcodeRegistry
20+
{
21+
/** @var array<int<1, 15>, class-string> $map */
22+
private array $map = [
23+
1 => Text::class,
24+
2 => Binary::class,
25+
8 => Close::class,
26+
9 => Ping::class,
27+
10 => Pong::class,
28+
];
29+
30+
/**
31+
* @param class-string $classname
32+
* @return int<1, 15>
33+
* @throws BadOpcodeException
34+
*/
35+
public function getOpcode(string $classname): int
36+
{
37+
$opcode = array_search($classname, $this->map);
38+
if (!is_int($opcode) || $opcode < 1 || $opcode > 15) {
39+
throw new BadOpcodeException(sprintf(
40+
'Opcode must be integer in range 1-15, %s provided',
41+
json_encode($opcode)
42+
));
43+
}
44+
return $opcode;
45+
}
46+
47+
/**
48+
* @param int<1, 15> $opcode
49+
* @return Message
50+
* @throws BadOpcodeException
51+
*/
52+
public function createMessage(int $opcode): Message
53+
{
54+
if ($opcode < 1 || $opcode > 15) {
55+
throw new BadOpcodeException(sprintf(
56+
'Opcode must be integer in range 1-15, %s provided',
57+
json_encode($opcode)
58+
));
59+
}
60+
$classname = $this->map[$opcode] ?? null;
61+
if (!is_string($classname) || !class_exists($classname)) {
62+
throw new BadOpcodeException(sprintf(
63+
'Implementation class %s for opcode %s not found',
64+
json_encode($classname),
65+
$opcode
66+
));
67+
}
68+
return new $classname();
69+
}
70+
71+
/**
72+
* @param int<1, 15> $opcode
73+
* @param class-string $classname
74+
* @throws BadOpcodeException
75+
*/
76+
public function register(int $opcode, string $classname): void
77+
{
78+
if ($opcode < 1 || $opcode > 15) {
79+
throw new RangeException(sprintf(
80+
'Opcode must be integer in range 1-15, %s provided',
81+
json_encode($opcode)
82+
));
83+
}
84+
if (empty($classname) || !class_exists($classname)) {
85+
throw new DomainException(sprintf(
86+
'Implementation class %s not found',
87+
json_encode($classname)
88+
));
89+
}
90+
$class = new $classname();
91+
if (!$class instanceof Message) {
92+
throw new DomainException(sprintf(
93+
'Implementation class %s must extend %s',
94+
$class::class,
95+
Message::class,
96+
));
97+
}
98+
$this->map[$opcode] = $classname;
99+
}
100+
}

tests/suites/client/ClientErrorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public function testReceiveBadOpcode(): void
108108
return 'Test message';
109109
});
110110
$this->expectException(BadOpcodeException::class);
111-
$this->expectExceptionMessage("Invalid opcode '15' provided");
111+
$this->expectExceptionMessage("Implementation class null for opcode 15 not found");
112112
$message = $client->receive();
113113
}
114114

0 commit comments

Comments
 (0)