Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to ext-websocket are documented here.

## 0.10.0 - 2026-05-18

### Added

- Added `WebSocket\ServerOptions` for typed server configuration.

### Changed

- Updated `WebSocket\Server` to accept `ServerOptions` while keeping array options supported.
- Updated examples and production documentation to prefer `ServerOptions`.

## 0.9.1 - 2026-05-18

### Added
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Native WebSocket extension for PHP.

`ext-websocket` keeps RFC 6455 protocol work in C and exposes a small PHP API for synchronous PHP code, async runtimes, and the native server runtime included in the extension.

Current version: `0.9.1`.
Current version: `0.10.0`.

## Requirements

Expand Down Expand Up @@ -104,11 +104,13 @@ Options:
| `maxMessageSize` | Maximum incoming text/binary message size; defaults to 16 MiB |
| `maxQueuedBytes` | Maximum outgoing queued bytes per connection; defaults to 16 MiB |

Prefer `WebSocket\ServerOptions` for explicit configuration. Associative arrays remain supported for compatibility.

Methods:

| Method | Description |
|---|---|
| `__construct(array $options = [])` | Create a server |
| `__construct(ServerOptions\|array $options = [])` | Create a server |
| `listen(string $host, int $port): void` | Bind address for `run()` |
| `onOpen(Closure $handler): void` | Register upgraded connection callback |
| `onMessage(Closure $handler): void` | Register text/binary message callback |
Expand Down
8 changes: 4 additions & 4 deletions docs/production.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ Recommended process managers:
Set explicit limits for your workload:

```php
$server = new WebSocket\Server([
'maxMessageSize' => 1024 * 1024,
'maxQueuedBytes' => 8 * 1024 * 1024,
]);
$server = new WebSocket\Server(new WebSocket\ServerOptions(
maxMessageSize: 1024 * 1024,
maxQueuedBytes: 8 * 1024 * 1024,
));
```

`maxMessageSize` protects incoming frames and fragmented messages. `maxQueuedBytes` protects memory when a client reads slowly and outgoing writes need to be queued.
Expand Down
7 changes: 4 additions & 3 deletions examples/run_server.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use WebSocket\MessageType;
use WebSocket\Protocol;
use WebSocket\Server;
use WebSocket\ServerOptions;

if (!extension_loaded('websocket')) {
fwrite(STDERR, "The websocket extension is not loaded.\n");
Expand All @@ -27,9 +28,9 @@
exit(1);
}

$server = new Server([
'maxMessageSize' => 1024 * 1024,
]);
$server = new Server(new ServerOptions(
maxMessageSize: 1024 * 1024,
));

$server->listen($host, $port);

Expand Down
3 changes: 2 additions & 1 deletion php_websocket.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
extern zend_module_entry websocket_module_entry;
#define phpext_websocket_ptr &websocket_module_entry

#define PHP_WEBSOCKET_VERSION "0.9.1"
#define PHP_WEBSOCKET_VERSION "0.10.0"
#define WEBSOCKET_HTTP_MAX_REQUEST_SIZE 8192
#define WEBSOCKET_DEFAULT_MAX_MESSAGE_SIZE (16 * 1024 * 1024)
#define WEBSOCKET_DEFAULT_MAX_QUEUED_BYTES (16 * 1024 * 1024)
Expand Down Expand Up @@ -132,6 +132,7 @@ typedef struct _websocket_connection_object {
} websocket_connection_object;

extern zend_class_entry *websocket_server_ce;
extern zend_class_entry *websocket_server_options_ce;
extern zend_class_entry *websocket_connection_ce;
extern zend_class_entry *websocket_message_type_ce;
extern zend_class_entry *websocket_frame_ce;
Expand Down
15 changes: 15 additions & 0 deletions tests/001-contracts.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ websocket
--FILE--
<?php
var_dump(class_exists(WebSocket\Server::class));
var_dump(class_exists(WebSocket\ServerOptions::class));
var_dump(class_exists(WebSocket\Connection::class));
var_dump(enum_exists(WebSocket\MessageType::class));
var_dump(class_exists(WebSocket\Frame::class));
Expand All @@ -13,6 +14,15 @@ var_dump(class_exists(WebSocket\Protocol::class));
var_dump(method_exists(WebSocket\Server::class, 'send'));
var_dump(method_exists(WebSocket\Server::class, 'close'));
var_dump((new ReflectionMethod(WebSocket\Connection::class, 'send'))->getNumberOfParameters());
var_dump((new ReflectionMethod(WebSocket\ServerOptions::class, '__construct'))->getNumberOfParameters());
$options = new WebSocket\ServerOptions(maxMessageSize: 1024, maxQueuedBytes: 2048);
var_dump($options->maxMessageSize);
var_dump($options->maxQueuedBytes);
try {
new WebSocket\ServerOptions(maxMessageSize: 0);
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}
var_dump((new ReflectionMethod(WebSocket\Frame::class, '__construct'))->getNumberOfParameters());
var_dump((new ReflectionMethod(WebSocket\CloseFrame::class, '__construct'))->getNumberOfParameters());

Expand All @@ -31,9 +41,14 @@ bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)
int(2)
int(2)
int(1024)
int(2048)
WebSocket\ServerOptions::__construct(): Argument #1 ($maxMessageSize) must be at least 1
int(3)
int(2)
bool(true)
2 changes: 1 addition & 1 deletion tests/011-server-fragmentation-size.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ $serverCode = <<<'PHP'
use WebSocket\Connection;
use WebSocket\Server;

$server = new Server(['maxMessageSize' => 5]);
$server = new Server(new WebSocket\ServerOptions(maxMessageSize: 5));
$server->listen('127.0.0.1', PORT_PLACEHOLDER);

$server->onOpen(static function (Connection $connection): void {
Expand Down
1 change: 1 addition & 0 deletions websocket.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
ZEND_DECLARE_MODULE_GLOBALS(websocket)

zend_class_entry *websocket_server_ce;
zend_class_entry *websocket_server_options_ce;
zend_class_entry *websocket_connection_ce;
zend_class_entry *websocket_message_type_ce;
zend_class_entry *websocket_frame_ce;
Expand Down
32 changes: 30 additions & 2 deletions websocket.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ final class Server
* - maxMessageSize: maximum accepted text/binary message size in bytes.
* - maxQueuedBytes: maximum queued outgoing bytes per connection.
*
* @param array{maxMessageSize?: positive-int, maxQueuedBytes?: positive-int} $options
* @param ServerOptions|array{maxMessageSize?: positive-int, maxQueuedBytes?: positive-int} $options
*/
public function __construct(array $options = []) {}
public function __construct(ServerOptions|array $options = []) {}

/**
* Bind the TCP listener used by run().
Expand Down Expand Up @@ -86,6 +86,34 @@ public function stop(): void {}
public function getDriver(): string {}
}

/**
* Explicit configuration for WebSocket\Server.
*/
final class ServerOptions
{
/**
* Maximum accepted text/binary message size in bytes.
*
* @var positive-int
*/
public readonly int $maxMessageSize;

/**
* Maximum queued outgoing bytes per connection.
*
* @var positive-int
*/
public readonly int $maxQueuedBytes;

/**
* @param positive-int $maxMessageSize
* @param positive-int $maxQueuedBytes
*
* @throws \ValueError If a limit is less than 1.
*/
public function __construct(int $maxMessageSize = 16 * 1024 * 1024, int $maxQueuedBytes = 16 * 1024 * 1024) {}
}

/**
* Runtime connection accepted by WebSocket\Server.
*/
Expand Down
31 changes: 30 additions & 1 deletion websocket_arginfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
#include "Zend/zend_enum.h"

ZEND_BEGIN_ARG_INFO_EX(arginfo_class_WebSocket_Server___construct, 0, 0, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 0, "[]")
{ "options", ZEND_TYPE_INIT_CLASS_CONST_MASK("WebSocket\\ServerOptions", MAY_BE_ARRAY | _ZEND_ARG_INFO_FLAGS(0, 0, 0)), "[]" },
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_class_WebSocket_ServerOptions___construct, 0, 0, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, maxMessageSize, IS_LONG, 0, "16 * 1024 * 1024")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, maxQueuedBytes, IS_LONG, 0, "16 * 1024 * 1024")
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Server_listen, 0, 2, IS_VOID, 0)
Expand Down Expand Up @@ -88,6 +93,7 @@ ZEND_METHOD(WebSocket_Server, onError);
ZEND_METHOD(WebSocket_Server, run);
ZEND_METHOD(WebSocket_Server, stop);
ZEND_METHOD(WebSocket_Server, getDriver);
ZEND_METHOD(WebSocket_ServerOptions, __construct);
ZEND_METHOD(WebSocket_Connection, send);
ZEND_METHOD(WebSocket_Connection, close);
ZEND_METHOD(WebSocket_Connection, isOpen);
Expand All @@ -112,6 +118,11 @@ static const zend_function_entry class_WebSocket_Server_methods[] = {
ZEND_FE_END
};

static const zend_function_entry class_WebSocket_ServerOptions_methods[] = {
ZEND_ME(WebSocket_ServerOptions, __construct, arginfo_class_WebSocket_ServerOptions___construct, ZEND_ACC_PUBLIC)
ZEND_FE_END
};

static const zend_function_entry class_WebSocket_Connection_methods[] = {
ZEND_ME(WebSocket_Connection, send, arginfo_class_WebSocket_Connection_send, ZEND_ACC_PUBLIC)
ZEND_ME(WebSocket_Connection, close, arginfo_class_WebSocket_Connection_close, ZEND_ACC_PUBLIC)
Expand Down Expand Up @@ -148,6 +159,24 @@ static zend_class_entry *register_class_WebSocket_Server(void)
return class_entry;
}

static zend_class_entry *register_class_WebSocket_ServerOptions(void)
{
zend_class_entry ce, *class_entry;
zval property_maxMessageSize_default_value;
zval property_maxQueuedBytes_default_value;

INIT_NS_CLASS_ENTRY(ce, "WebSocket", "ServerOptions", class_WebSocket_ServerOptions_methods);
class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL);

ZVAL_UNDEF(&property_maxMessageSize_default_value);
zend_declare_typed_property(class_entry, zend_string_init("maxMessageSize", sizeof("maxMessageSize") - 1, 1), &property_maxMessageSize_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));

ZVAL_UNDEF(&property_maxQueuedBytes_default_value);
zend_declare_typed_property(class_entry, zend_string_init("maxQueuedBytes", sizeof("maxQueuedBytes") - 1, 1), &property_maxQueuedBytes_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));

return class_entry;
}

static zend_class_entry *register_class_WebSocket_Connection(void)
{
zend_class_entry ce, *class_entry;
Expand Down
31 changes: 30 additions & 1 deletion websocket_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,44 @@ PHP_METHOD(WebSocket_Server, __construct)

ZEND_PARSE_PARAMETERS_START(0, 1)
Z_PARAM_OPTIONAL
Z_PARAM_ARRAY(options)
Z_PARAM_ZVAL(options)
ZEND_PARSE_PARAMETERS_END();

if (options) {
if (Z_TYPE_P(options) != IS_ARRAY && (Z_TYPE_P(options) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(options), websocket_server_options_ce))) {
zend_argument_type_error(1, "must be of type array|WebSocket\\ServerOptions, %s given", websocket_zval_value_name(options));
RETURN_THROWS();
}
ZVAL_COPY(&intern->options, options);
} else {
array_init(&intern->options);
}
}

PHP_METHOD(WebSocket_ServerOptions, __construct)
{
zend_long max_message_size = WEBSOCKET_DEFAULT_MAX_MESSAGE_SIZE;
zend_long max_queued_bytes = WEBSOCKET_DEFAULT_MAX_QUEUED_BYTES;

ZEND_PARSE_PARAMETERS_START(0, 2)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(max_message_size)
Z_PARAM_LONG(max_queued_bytes)
ZEND_PARSE_PARAMETERS_END();

if (max_message_size < 1) {
zend_argument_value_error(1, "must be at least 1");
RETURN_THROWS();
}
if (max_queued_bytes < 1) {
zend_argument_value_error(2, "must be at least 1");
RETURN_THROWS();
}

zend_update_property_long(websocket_server_options_ce, Z_OBJ_P(ZEND_THIS), "maxMessageSize", sizeof("maxMessageSize") - 1, max_message_size);
zend_update_property_long(websocket_server_options_ce, Z_OBJ_P(ZEND_THIS), "maxQueuedBytes", sizeof("maxQueuedBytes") - 1, max_queued_bytes);
}

PHP_METHOD(WebSocket_Server, listen)
{
zend_string *host;
Expand Down Expand Up @@ -234,6 +262,7 @@ PHP_METHOD(WebSocket_Server, getDriver)
void websocket_register_server_class(void)
{
websocket_server_ce = register_class_WebSocket_Server();
websocket_server_options_ce = register_class_WebSocket_ServerOptions();
websocket_server_ce->create_object = websocket_server_create_object;

memcpy(&websocket_server_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
Expand Down
30 changes: 18 additions & 12 deletions websocket_server_runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -370,13 +370,16 @@ static void websocket_connection_discard_read_bytes(websocket_connection_object
static size_t websocket_server_max_message_size(websocket_server_object *intern)
{
zval *value;
zval rv;

if (Z_TYPE(intern->options) != IS_ARRAY) {
return WEBSOCKET_DEFAULT_MAX_MESSAGE_SIZE;
}

value = zend_hash_str_find(Z_ARRVAL(intern->options), "maxMessageSize", sizeof("maxMessageSize") - 1);
if (!value) {
if (Z_TYPE(intern->options) == IS_ARRAY) {
value = zend_hash_str_find(Z_ARRVAL(intern->options), "maxMessageSize", sizeof("maxMessageSize") - 1);
if (!value) {
return WEBSOCKET_DEFAULT_MAX_MESSAGE_SIZE;
}
} else if (Z_TYPE(intern->options) == IS_OBJECT && instanceof_function(Z_OBJCE(intern->options), websocket_server_options_ce)) {
value = zend_read_property(websocket_server_options_ce, Z_OBJ(intern->options), "maxMessageSize", sizeof("maxMessageSize") - 1, 0, &rv);
} else {
return WEBSOCKET_DEFAULT_MAX_MESSAGE_SIZE;
}

Expand All @@ -391,13 +394,16 @@ static size_t websocket_server_max_message_size(websocket_server_object *intern)
static size_t websocket_server_max_queued_bytes(websocket_server_object *intern)
{
zval *value;
zval rv;

if (Z_TYPE(intern->options) != IS_ARRAY) {
return WEBSOCKET_DEFAULT_MAX_QUEUED_BYTES;
}

value = zend_hash_str_find(Z_ARRVAL(intern->options), "maxQueuedBytes", sizeof("maxQueuedBytes") - 1);
if (!value) {
if (Z_TYPE(intern->options) == IS_ARRAY) {
value = zend_hash_str_find(Z_ARRVAL(intern->options), "maxQueuedBytes", sizeof("maxQueuedBytes") - 1);
if (!value) {
return WEBSOCKET_DEFAULT_MAX_QUEUED_BYTES;
}
} else if (Z_TYPE(intern->options) == IS_OBJECT && instanceof_function(Z_OBJCE(intern->options), websocket_server_options_ce)) {
value = zend_read_property(websocket_server_options_ce, Z_OBJ(intern->options), "maxQueuedBytes", sizeof("maxQueuedBytes") - 1, 0, &rv);
} else {
return WEBSOCKET_DEFAULT_MAX_QUEUED_BYTES;
}

Expand Down
Loading