From 5d81a5957e9fb0ecf00ce2d1601330bc3435a9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Fri, 5 Sep 2025 10:58:58 +0200 Subject: [PATCH 01/16] Remove deprecated code --- README.md | 1 + docs/Changelog.md | 8 +++++++ docs/Class_Synopsis.md | 4 ---- docs/Index.md | 1 + docs/Migrate_3_4.md | 18 +++++++++++++++ docu.json | 10 +++++--- src/Client.php | 12 ---------- src/Http/HttpHandler.php | 15 +----------- src/Trait/ListenerTrait.php | 12 ---------- tests/mock/EchoLog.php | 25 -------------------- tests/suites/client/ClientTest.php | 33 +-------------------------- tests/suites/http/HttpHandlerTest.php | 24 ------------------- tests/suites/server/ServerTest.php | 14 +----------- 13 files changed, 38 insertions(+), 139 deletions(-) create mode 100644 docs/Migrate_3_4.md delete mode 100644 tests/mock/EchoLog.php diff --git a/README.md b/README.md index b3bc98f..f95913e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ and is maintained by Sören Jensen, who has been maintaining the original since * [Contributing](docs/Contributing.md) - Contributors and requirements * [Examples](docs/Examples.md) - Examples * [v2 -> v3](docs/Migrate_2_3.md) - How to migrate from v2 to v3 +* [v3 -> v4](docs/Migrate_3_4.md) - How to migrate from v3 to v4 ## Installing diff --git a/docs/Changelog.md b/docs/Changelog.md index f72f11d..8596c67 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -2,6 +2,14 @@ # Websocket: Changelog +## `v4.0` + + > PHP version `^8.1` + +### `4.0.0` + + * Remove deprecated code (@sirn-se) + ## `v3.6` > PHP version `^8.1` diff --git a/docs/Class_Synopsis.md b/docs/Class_Synopsis.md index 298974c..5a302aa 100644 --- a/docs/Class_Synopsis.md +++ b/docs/Class_Synopsis.md @@ -52,11 +52,9 @@ class WebSocket\Client implements Psr\Log\LoggerAwareInterface, Stringable // Connection wrapper methods public function getName(): string|null; public function getRemoteName(): string|null; - public function getMeta(string $key): mixed; // @deprecated public function getHandshakeResponse(): WebSocket\Http\Response|null; // Listener methods - public function onConnect(Closure $closure): self; // @deprecated public function onDisconnect(Closure $closure): self; public function onHandshake(Closure $closure): self; public function onText(Closure $closure): self; @@ -115,7 +113,6 @@ class WebSocket\Server implements Psr\Log\LoggerAwareInterface, Stringable public function disconnect(): void; // Listener methods - public function onConnect(Closure $closure): self; // @deprecated public function onDisconnect(Closure $closure): self; public function onHandshake(Closure $closure): self; public function onText(Closure $closure): self; @@ -315,7 +312,6 @@ class WebSocket\Frame\FrameHandler implements LoggerAwareInterface, Stringable { public function __construct(Phrity\Net\SocketStream $stream, bool $pushMasked, bool $pullMaskedRequired); public function __toString(): string; - public function setLogger(Psr\Log\LoggerInterface $logger): void; // @deprecated public function pull(): WebSocket\Frame\Frame; public function push(WebSocket\Frame\Frame $frame): int; } diff --git a/docs/Index.md b/docs/Index.md index 6df5b31..f82a140 100644 --- a/docs/Index.md +++ b/docs/Index.md @@ -23,3 +23,4 @@ ## Migration * [v2 -> v3](Migrate_2_3.md) - How to migrate from v2 to v3 +* [v3 -> v4](Migrate_3_4.md) - How to migrate from v3 to v4 diff --git a/docs/Migrate_3_4.md b/docs/Migrate_3_4.md new file mode 100644 index 0000000..06759b8 --- /dev/null +++ b/docs/Migrate_3_4.md @@ -0,0 +1,18 @@ +[Documentation](Index.md) / Migration v3 -> v4 + +# Websocket: Migration v3 -> v4 + +Version 4.x has few changes compared to previous version. + +## Breaking changes + + +## Removed deprecated code + +* Client `getMeta()` - removed +* Client `onConnect()` - use `onHandshake()` instead +* Server `onConnect()` - use `onHandshake()` instead +* FrameHandler `setLogger()` - removed + +## Extending + diff --git a/docu.json b/docu.json index 4331769..29b1e36 100644 --- a/docu.json +++ b/docu.json @@ -13,7 +13,7 @@ { "name": "Server", "source": "docs/Server.md" - }, + } ], "name": "Resources", "content": [ @@ -64,7 +64,7 @@ { "name": "Creating your own middleware", "source": "docs/Middleware/Creating.md" - }, + } ] }, ], @@ -85,7 +85,7 @@ { "name": "Class synopsis", "source": "docs/Class_Synopsis.md" - }, + } ], "name": "Migration", "content": [ @@ -93,6 +93,10 @@ "name": "Migration v2 -> v3", "source": "docs/Migrate_2_3.md" }, + { + "name": "Migration v3 -> v4", + "source": "docs/Migrate_3_4.md" + } ] }, { diff --git a/src/Client.php b/src/Client.php index b872f58..41f53bc 100644 --- a/src/Client.php +++ b/src/Client.php @@ -554,18 +554,6 @@ public function getRemoteName(): string|null return $this->isConnected() ? $this->connection?->getRemoteName() : null; } - /** - * Get meta value on connection. - * @param string $key Meta key - * @return mixed Meta value - * @deprecated Will be removed in v4 - */ - public function getMeta(string $key): mixed - { - trigger_error('Client.getMeta is deprecated and will be removed in v4.', E_USER_DEPRECATED); - return $this->isConnected() ? $this->connection?->getMeta($key) : null; - } - /** * Get Response for handshake procedure. * @return ResponseInterface|null Handshake. diff --git a/src/Http/HttpHandler.php b/src/Http/HttpHandler.php index 726d790..bf9b453 100644 --- a/src/Http/HttpHandler.php +++ b/src/Http/HttpHandler.php @@ -17,10 +17,6 @@ ServerRequestFactoryInterface, UriFactoryInterface, }; -use Psr\Log\{ - LoggerInterface, - LoggerAwareInterface, -}; use RuntimeException; use Stringable; use WebSocket\Trait\StringableTrait; @@ -28,9 +24,8 @@ /** * WebSocket\Http\HttpHandler class. * Reads and writes HTTP message to/from stream. - * @deprecated Remove LoggerAwareInterface in v4 */ -class HttpHandler implements LoggerAwareInterface, Stringable +class HttpHandler implements Stringable { use StringableTrait; @@ -54,14 +49,6 @@ public function __construct( $this->uriFactory = $uriFactory ?? new DefaultHttpFactory(); } - /** - * @deprecated Remove in v4 - */ - public function setLogger(LoggerInterface $logger): void - { - trigger_error('HttpHandler.setLogger is deprecated and will be removed in v4.', E_USER_DEPRECATED); - } - /** * @throws RuntimeException */ diff --git a/src/Trait/ListenerTrait.php b/src/Trait/ListenerTrait.php index 222c35e..c7d2d6c 100644 --- a/src/Trait/ListenerTrait.php +++ b/src/Trait/ListenerTrait.php @@ -33,18 +33,6 @@ trait ListenerTrait /** @var array $listeners */ private array $listeners = []; - /** - * @param Closure(T, Connection, RequestInterface|ResponseInterface): void $closure - * @deprecated Will be removed in v4 - */ - public function onConnect(Closure $closure): self - { - $msg = 'onConnect() is deprecated and will be removed in v4. Use onHandshake() instead.'; - trigger_error($msg, E_USER_DEPRECATED); - $this->listeners['connect'] = $closure; - return $this; - } - /** @param Closure(T, Connection): void $closure */ public function onDisconnect(Closure $closure): self { diff --git a/tests/mock/EchoLog.php b/tests/mock/EchoLog.php deleted file mode 100644 index cada19b..0000000 --- a/tests/mock/EchoLog.php +++ /dev/null @@ -1,25 +0,0 @@ -assertInstanceOf(Request::class, $request); $this->assertInstanceOf(Response::class, $response); $this->assertTrue($client->isRunning()); - }); - $handler->withAll(function () use ($client) { - $client->onConnect(function ($client, $connection, $response) { - $this->assertInstanceOf(Client::class, $client); - $this->assertInstanceOf(Connection::class, $connection); - $this->assertInstanceOf(Response::class, $response); - $this->assertTrue($client->isRunning()); - $client->stop(); - }); - }, function (array $errors) { - $this->assertEquals( - 'onConnect() is deprecated and will be removed in v4. Use onHandshake() instead.', - $errors[0]->getMessage() - ); + $client->stop(); }); $client->onText(function ($client, $connection, $message) { $this->assertInstanceOf(Client::class, $client); @@ -1161,22 +1148,4 @@ public function testUnresolvableError(): void $client->start(); unset($client); } - - public function testDeprecatedMeta(): void - { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); - - $errorHandler = new ErrorHandler(); - $errorHandler->withAll(function () use ($client) { - $this->assertNull($client->getMeta('metadata')); - }, function (array $errors) { - $this->assertEquals( - 'Client.getMeta is deprecated and will be removed in v4.', - $errors[0]->getMessage() - ); - }); - unset($client); - } } diff --git a/tests/suites/http/HttpHandlerTest.php b/tests/suites/http/HttpHandlerTest.php index 4ebf13a..bb6ca9a 100644 --- a/tests/suites/http/HttpHandlerTest.php +++ b/tests/suites/http/HttpHandlerTest.php @@ -301,28 +301,4 @@ public function testPushUnsupported(): void fclose($temp); } - - public function testDeprecatedLogger(): void - { - $temp = tmpfile(); - - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $stream = new SocketStream($temp); - $httpHandler = new HttpHandler($stream); - - $logger = new ConsoleLogger(); - $errorHandler = new ErrorHandler(); - $errorHandler->withAll(function () use ($httpHandler, $logger) { - $httpHandler->setLogger($logger); - }, function (array $errors) { - $this->assertEquals( - 'HttpHandler.setLogger is deprecated and will be removed in v4.', - $errors[0]->getMessage() - ); - }); - - fclose($temp); - } } diff --git a/tests/suites/server/ServerTest.php b/tests/suites/server/ServerTest.php index 752153e..ef188b1 100644 --- a/tests/suites/server/ServerTest.php +++ b/tests/suites/server/ServerTest.php @@ -91,19 +91,7 @@ public function testListeners(): void $this->assertInstanceOf(Connection::class, $connection); $this->assertInstanceOf(ServerRequest::class, $request); $this->assertInstanceOf(Response::class, $response); - }); - $handler->withAll(function () use ($server) { - $server->onConnect(function ($server, $connection, $request) { - $this->assertInstanceOf(Server::class, $server); - $this->assertInstanceOf(Connection::class, $connection); - $this->assertInstanceOf(ServerRequest::class, $request); - $server->stop(); - }); - }, function (array $errors) { - $this->assertEquals( - 'onConnect() is deprecated and will be removed in v4. Use onHandshake() instead.', - $errors[0]->getMessage() - ); + $server->stop(); }); $server->onText(function ($server, $connection, $message) { $this->assertInstanceOf(Server::class, $server); From 1c5d0fcd3546e86a4a809bea9d65e457244a3d4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Sat, 6 Sep 2025 14:56:34 +0200 Subject: [PATCH 02/16] v4 wip: server watcher --- composer.json | 13 +- docs/Class_Synopsis.md | 4 +- docs/Examples.md | 27 ++-- docs/Migrate_3_4.md | 2 + src/Client.php | 10 +- src/Constant.php | 4 +- src/Runtime/Watcher.php | 67 +++++++++ src/Server.php | 140 +++++++++--------- tests/mock/MockStreamTrait.php | 32 ++++- tests/suites/client/ClientTest.php | 32 ++--- tests/suites/client/ConfigTest.php | 18 +-- tests/suites/server/ConfigErrorTest.php | 2 + tests/suites/server/ConfigTest.php | 64 ++++----- tests/suites/server/HandshakeTest.php | 90 +++++++----- tests/suites/server/ServerTest.php | 183 +++++++++++++++--------- 15 files changed, 421 insertions(+), 267 deletions(-) create mode 100644 src/Runtime/Watcher.php diff --git a/composer.json b/composer.json index 362f0b4..3a83665 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ ], "autoload": { "psr-4": { - "WebSocket\\": "src/" + "WebSocket\\": "src/", + "X\\": "src-x/" } }, "autoload-dev": { @@ -28,7 +29,7 @@ "require": { "php": "^8.1", "phrity/net-uri": "^2.1", - "phrity/net-stream": "^2.3", + "phrity/net-stream": "dev-ws4 as 2.4.0", "psr/http-message": "^1.1 | ^2.0", "psr/log": "^1.0 | ^2.0 | ^3.0" }, @@ -38,12 +39,14 @@ "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.0 | ^11.0 | ^12.0", "phrity/logger-console": "^1.0", - "phrity/net-mock": "^2.3", + "phrity/net-mock": "dev-ws4 as 2.4.0", "phrity/util-errorhandler": "^1.1", "robiningelbrecht/phpunit-coverage-tools": "^1.9", "squizlabs/php_codesniffer": "^3.5" }, - "suggests": { - "ext-zlib": "Required for per-message deflate compression" + "suggest": { + "ext-zlib": "Required for per-message deflate compression", + "psr/http-message-implementation": "Use a complete http-message implementation", + "psr/log-implementation": "Use a logger implementation" } } diff --git a/docs/Class_Synopsis.md b/docs/Class_Synopsis.md index 5a302aa..05782da 100644 --- a/docs/Class_Synopsis.md +++ b/docs/Class_Synopsis.md @@ -24,7 +24,7 @@ class WebSocket\Client implements Psr\Log\LoggerAwareInterface, Stringable public function getFrameSize(): int; public function setPersistent(bool $persistent): self; public function getContext(): Phrity\Net\Context; - public function setContext(Phrity\Net\Context|array $context): self; + public function setContext(Phrity\Net\Context $context): self; public function addHeader(string $name, string $content): self; public function addMiddleware(WebSocket\Middleware\MiddlewareInterface $middleware): self; @@ -87,7 +87,7 @@ class WebSocket\Server implements Psr\Log\LoggerAwareInterface, Stringable public function getScheme(): string; public function isSsl(): bool; public function getContext(): Phrity\Net\Context; - public function setContext(Phrity\Net\Context|array $context): self; + public function setContext(Phrity\Net\Context $context): self; public function getConnectionCount(): int; public function getConnections(): array; public function getReadableConnections(): array; diff --git a/docs/Examples.md b/docs/Examples.md index 83f8e74..f2e58f1 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -8,7 +8,7 @@ Here are some examples on how to use the WebSocket library. Client and Server accepts a [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger to be attached. -The ConsoleLogger in example is useful for debugging. +The `phrity/logger-console` in example is useful for debugging. For production, use a logger that stores or accumulates log data. ```php @@ -189,19 +189,24 @@ use WebSocket\Server; $port = 443; // Port to use $logger = new Logger(); // PSR-3 Logger to use +// Create context +$context = new Phrity\Net\Context(); +// Set up SSL with CA certificate +$context->setOptions(['ssl' => [ + 'local_cert' => 'certs/CA/acs-ws-server.crt', + 'local_pk' => 'certs/CA/acs-ws-server.key', + 'cafile' => 'certs/CA/ca.crt', + 'verify_peer' => true, // if false, accept SSL handshake without client certificate + 'verify_peer_name' => false, + 'allow_self_signed' => false, + 'capture_peer_cert' => true, +]]); + // Create server $server = new Server(port: $port, ssl: true); $server - // Set up SSL with CA certificate - ->setContext(['ssl' => [ - 'local_cert' => 'certs/CA/acs-ws-server.crt', - 'local_pk' => 'certs/CA/acs-ws-server.key', - 'cafile' => 'certs/CA/ca.crt', - 'verify_peer' => true, // if false, accept SSL handshake without client certificate - 'verify_peer_name' => false, - 'allow_self_signed' => false, - 'capture_peer_cert' => true, - ]]) + // Add the context + ->setContext($context) // Add standard middlewares ->addMiddleware(new CloseHandler()) ->addMiddleware(new PingResponder()) diff --git a/docs/Migrate_3_4.md b/docs/Migrate_3_4.md index 06759b8..c473920 100644 --- a/docs/Migrate_3_4.md +++ b/docs/Migrate_3_4.md @@ -11,7 +11,9 @@ Version 4.x has few changes compared to previous version. * Client `getMeta()` - removed * Client `onConnect()` - use `onHandshake()` instead +* Client `setContext()` - no longer accepts array input * Server `onConnect()` - use `onHandshake()` instead +* Server `setContext()` - no longer accepts array input * FrameHandler `setLogger()` - removed ## Extending diff --git a/src/Client.php b/src/Client.php index 41f53bc..6ce3d61 100644 --- a/src/Client.php +++ b/src/Client.php @@ -236,17 +236,13 @@ public function setPersistent(bool $persistent): self /** * Set stream context. - * @param Context|array $context Context or options as array + * @param Context $context Context or options as array * @see https://www.php.net/manual/en/context.php * @return self */ - public function setContext(Context|array $context): self + public function setContext(Context $context): self { - if ($context instanceof Context) { - $this->context = $context; - } else { - $this->context->setOptions($context); - } + $this->context = $context; return $this; } diff --git a/src/Constant.php b/src/Constant.php index f9b997a..49dfe0e 100644 --- a/src/Constant.php +++ b/src/Constant.php @@ -8,8 +8,8 @@ namespace WebSocket; /** - * WebSocket\Connection class. - * A client/server connection, wrapping socket stream. + * WebSocket\Constant class. + * Defines global constants. */ interface Constant { diff --git a/src/Runtime/Watcher.php b/src/Runtime/Watcher.php new file mode 100644 index 0000000..6cab4d7 --- /dev/null +++ b/src/Runtime/Watcher.php @@ -0,0 +1,67 @@ + $selects */ + private array $selects = []; + + public function __construct(StreamCollection $streamCollection) + { + $this->streamCollection = $streamCollection; + } + + public function attach(string $key, StreamInterface $attach, Closure $onSelect): void + { + $this->selects[$key] = $onSelect; + $this->streamCollection->attach($attach, $key); + } + + public function detach(string $key): void + { + if (array_key_exists($key, $this->selects)) { + unset($this->selects[$key]); + } + $this->streamCollection->detach($key); + } + + /** + * @throws ExceptionInterface + * @throws StreamException + */ + public function watch(float $timeout): void + { + $readables = $this->streamCollection->waitRead($timeout); + foreach ($readables as $key => $readable) { + /** + * @throws ExceptionInterface + * @throws StreamException + */ + call_user_func($this->selects[$key], $key, $readable); + } + } + + public function isEmpty(): bool + { + return $this->streamCollection->count() == 0; + } +} diff --git a/src/Server.php b/src/Server.php index 8b1bb8a..9b97612 100644 --- a/src/Server.php +++ b/src/Server.php @@ -52,6 +52,7 @@ SendMethodsTrait, StringableTrait }; +use WebSocket\Runtime\Watcher; /** * WebSocket\Server class. @@ -75,9 +76,7 @@ class Server implements LoggerAwareInterface, Stringable private Context $context; // Internal resources - private StreamFactory $streamFactory; private SocketServer|null $server = null; - private StreamCollection|null $streams = null; private bool $running = false; /** @var array $connections */ private array $connections = []; @@ -89,16 +88,24 @@ class Server implements LoggerAwareInterface, Stringable private ServerRequestFactoryInterface $serverRequestFactory; private UriFactoryInterface $uriFactory; + private StreamFactory $streamFactory; + private Watcher $watcher; /* ---------- Magic methods ------------------------------------------------------------------------------------ */ /** - * @param int $port Socket port to listen to + * @param int<0, 65535> $port Socket port to listen to * @param bool $ssl If SSL should be used + * @param StreamFactory|null $streamFactory + * @param Watcher|null $watcher * @throws InvalidArgumentException If invalid port provided */ - public function __construct(int $port = 80, bool $ssl = false) - { + public function __construct( + int $port = 80, + bool $ssl = false, + StreamFactory|null $streamFactory = null, + Watcher|null $watcher = null, + ) { if ($port < 0 || $port > 65535) { throw new InvalidArgumentException("Invalid port '{$port}' provided"); } @@ -106,8 +113,10 @@ public function __construct(int $port = 80, bool $ssl = false) $this->scheme = $ssl ? 'ssl' : 'tcp'; $this->initLogger(); $this->context = new Context(); - $this->setStreamFactory(new StreamFactory()); $this->responseFactory = $this->serverRequestFactory = $this->uriFactory = new DefaultHttpFactory(); + + $this->streamFactory = $streamFactory ?? new StreamFactory(); + $this->watcher = $watcher ?? new Watcher($this->streamFactory->createStreamCollection()); } /** @@ -298,17 +307,13 @@ public function getWritableConnections(): array /** * Set stream context. - * @param Context|array $context Context or options as array + * @param Context $context Context or options as array * @see https://www.php.net/manual/en/context.php * @return self */ - public function setContext(Context|array $context): self + public function setContext(Context $context): self { - if ($context instanceof Context) { - $this->context = $context; - } else { - $this->context->setOptions($context); - } + $this->context = $context; return $this; } @@ -374,7 +379,6 @@ public function send(Message $message): Message /** * Start server listener. - * @throws Throwable On low level error */ public function start(int|float|null $timeout = null): void { @@ -391,55 +395,16 @@ public function start(int|float|null $timeout = null): void $this->running = true; $this->logger->info("[server] Server is running"); - /** @var StreamCollection */ - $streams = $this->streams; - // Run handler while ($this->running) { try { // Clear closed connections $this->detachUnconnected(); - if (is_null($this->streams)) { + if ($this->watcher->isEmpty()) { $this->stop(); return; } - - // Get streams with readable content - $readables = $this->streams->waitRead($timeout ?? $this->timeout); - foreach ($readables as $key => $readable) { - try { - $connection = null; - // Accept new client connection - if ($readable instanceof SocketServer) { - $this->acceptSocket($readable); - continue; - } - // Read from connection - $connection = $this->connections[$key]; - $message = $connection->pullMessage(); - $this->dispatch($message->getOpcode(), [$this, $connection, $message]); - } catch (MessageLevelInterface $e) { - // Error, but keep connection open - $this->logger->error("[server] {$e->getMessage()}", ['exception' => $e]); - $this->dispatch('error', [$this, $connection, $e]); - } catch (ConnectionLevelInterface $e) { - // Error, disconnect connection - if ($connection) { - $this->streams()->detach($key); - unset($this->connections[$key]); - $connection->disconnect(); - } - $this->logger->error("[server] {$e->getMessage()}", ['exception' => $e]); - $this->dispatch('error', [$this, $connection, $e]); - } catch (CloseException $e) { - // Should close - if ($connection) { - $connection->close($e->getCloseStatus(), $e->getMessage()); - } - $this->logger->error("[server] {$e->getMessage()}", ['exception' => $e]); - $this->dispatch('error', [$this, $connection, $e]); - } - } + $this->watcher->watch($timeout ?? $this->timeout); foreach ($this->connections as $connection) { $connection->tick(); } @@ -458,6 +423,36 @@ public function start(int|float|null $timeout = null): void } } + private function selectHandler(string $key, SocketStream $stream): void + { + try { + // Read from connection + $connection = $this->connections[$key]; + $message = $connection->pullMessage(); + $this->dispatch($message->getOpcode(), [$this, $connection, $message]); + } catch (MessageLevelInterface $e) { + // Error, but keep connection open + $this->logger->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->dispatch('error', [$this, $connection, $e]); + } catch (ConnectionLevelInterface $e) { + // Error, disconnect connection + if ($connection) { + $this->watcher->detach($key); + unset($this->connections[$key]); + $connection->disconnect(); + } + $this->logger->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->dispatch('error', [$this, $connection, $e]); + } catch (CloseException $e) { + // Should close + if ($connection) { + $connection->close($e->getCloseStatus(), $e->getMessage()); + } + $this->logger->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->dispatch('error', [$this, $connection, $e]); + } + } + /** * Stop server listener (resumable). */ @@ -516,6 +511,7 @@ public function shutdown(int $closeStatus = 1001): void public function disconnect(): void { $this->running = false; + $this->watcher->detach('@server'); foreach ($this->connections as $connection) { $connection->disconnect(); $this->dispatch('disconnect', [$this, $connection]); @@ -524,7 +520,7 @@ public function disconnect(): void if ($this->server) { $this->server->close(); } - $this->server = $this->streams = null; + $this->server = null; $this->logger->info('[server] Server disconnected'); } @@ -537,17 +533,24 @@ protected function createSocketServer(): void try { $uri = new Uri("{$this->scheme}://0.0.0.0:{$this->port}"); $this->server = $this->streamFactory->createSocketServer($uri, $this->context); - $this->streams = $this->streamFactory->createStreamCollection(); - $this->streams->attach($this->server, '@server'); + /** @throws StreamException */ + $this->watcher->attach('@server', $this->server, function (string $key, SocketServer $socket) { + $this->acceptHandler($socket); + }); $this->logger->info("[server] Starting server on {$uri}."); - } catch (Throwable $e) { + } catch (StreamException $e) { $error = "Server failed to start: {$e->getMessage()}"; + $this->logger->error("[server] {$error}"); throw new ServerException($error); + } catch (Throwable $e) { + $error = "Server error: {$e->getMessage()}"; + $this->logger->error("[server] {$error}"); + throw $e; } } // Accept connection on socket server - protected function acceptSocket(SocketServer $socket): void + protected function acceptHandler(SocketServer $socket): void { if (!is_null($this->maxConnections) && $this->getConnectionCount() >= $this->maxConnections) { $this->logger->warning("[server] Denied connection, reached max {$this->maxConnections}"); @@ -556,8 +559,10 @@ protected function acceptSocket(SocketServer $socket): void try { /** @var SocketStream $stream */ $stream = $socket->accept(); - $name = $stream->getRemoteName(); - $this->streams()->attach($stream, $name); + $name = $stream->getRemoteName() ?? 'unknown'; + $this->watcher->attach($name, $stream, function ($key, $stream) { + $this->selectHandler($key, $stream); + }); $connection = new Connection( $stream, false, @@ -565,7 +570,7 @@ protected function acceptSocket(SocketServer $socket): void $this->isSsl(), $this->responseFactory, $this->serverRequestFactory, - $this->uriFactory + $this->uriFactory, ); } catch (StreamException $e) { throw new ConnectionFailureException("Server failed to accept: {$e->getMessage()}"); @@ -601,7 +606,7 @@ protected function detachUnconnected(): void { foreach ($this->connections as $key => $connection) { if (!$connection->isConnected()) { - $this->streams()->detach($key); + $this->watcher->detach($key); unset($this->connections[$key]); $this->logger->info("[server] Disconnected {$key}."); $this->dispatch('disconnect', [$this, $connection]); @@ -690,11 +695,4 @@ protected function performHandshake(Connection $connection): ServerRequest return $request; } - - protected function streams(): StreamCollection - { - /** @var StreamCollection $streams */ - $streams = $this->streams; - return $streams; - } } diff --git a/tests/mock/MockStreamTrait.php b/tests/mock/MockStreamTrait.php index 789b4d9..f4aa933 100644 --- a/tests/mock/MockStreamTrait.php +++ b/tests/mock/MockStreamTrait.php @@ -157,6 +157,13 @@ function (string $method, array $params) use ($host, $path, $headers): void { /* ---------- WebSocket Server combinded asserts --------------------------------------------------------------- */ + private function expectWsServerCreate(): void + { + $this->expectStreamFactory(); + $this->expectStreamFactoryCreateStreamCollection(); + $this->expectStreamCollection(); + } + /** * @param array $context */ @@ -177,8 +184,6 @@ private function expectWsServerSetup(string $scheme = 'tcp', int $port = 8000, a } $this->expectSocketServerGetMetadata(); - $this->expectStreamFactoryCreateStreamCollection(); - $this->expectStreamCollection(); $this->expectStreamCollectionAttach()->addAssert(function ($method, $params) { $this->assertEquals('@server', $params[1]); }); @@ -188,6 +193,29 @@ private function expectWsServerSetup(string $scheme = 'tcp', int $port = 8000, a * @param array $keys */ private function expectWsSelectConnections(array $keys = []): StackItem + { + $this->expectStreamCollectionCount(); + $this->expectStreamCollectionWaitRead()->setReturn(function ($params, $default, $collection) use ($keys) { + $keys = array_flip($keys); + $selected = new StreamCollection(); + foreach ($collection as $key => $stream) { + if (array_key_exists($key, $keys)) { + $selected->attach($stream, $key); + } + } + return $selected; + }); + $last = $this->expectStreamCollection(); + foreach ($keys as $key) { + $last = $this->expectStreamCollectionAttach(); + } + return $last; + } + + /** + * @param array $keys + */ + private function expectWsClientSelectConnections(array $keys = []): StackItem { $this->expectStreamCollectionWaitRead()->setReturn(function ($params, $default, $collection) use ($keys) { $keys = array_flip($keys); diff --git a/tests/suites/client/ClientTest.php b/tests/suites/client/ClientTest.php index b32df6b..bbae0e1 100644 --- a/tests/suites/client/ClientTest.php +++ b/tests/suites/client/ClientTest.php @@ -868,7 +868,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -878,7 +878,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -888,7 +888,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -898,7 +898,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -908,7 +908,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -918,7 +918,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -928,7 +928,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -976,7 +976,7 @@ public function testRunConnectionClosedException(): void $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -1005,7 +1005,7 @@ public function testRunClientException(): void $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -1025,7 +1025,7 @@ public function testRunExternalException(): void $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectWsSelectConnections(['localhost:8000'])->setReturn(function () { + $this->expectWsClientSelectConnections(['localhost:8000'])->setReturn(function () { throw new StreamException(1000); }); $this->expectSocketStreamIsConnected(); @@ -1049,7 +1049,7 @@ public function testCloseException(): void $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('gQA='); }); @@ -1057,7 +1057,7 @@ public function testCloseException(): void $this->expectSocketStreamIsReadable(); $this->expectSocketStreamCloseWrite(); $this->expectSocketStreamGetMetadata(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('iIk='); }); @@ -1089,7 +1089,7 @@ public function testReconnectException(): void }); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('gQA='); // text }); @@ -1098,7 +1098,7 @@ public function testReconnectException(): void $this->expectSocketStreamCloseWrite(); $this->expectSocketStreamGetMetadata(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('iBo='); // close }); @@ -1109,7 +1109,7 @@ public function testReconnectException(): void $this->expectSocketStreamClose(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('ggA='); // binary }); @@ -1136,7 +1136,7 @@ public function testUnresolvableError(): void $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectWsSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('gQA='); }); diff --git a/tests/suites/client/ConfigTest.php b/tests/suites/client/ConfigTest.php index ce922c2..3a69db9 100644 --- a/tests/suites/client/ConfigTest.php +++ b/tests/suites/client/ConfigTest.php @@ -207,23 +207,7 @@ public function testTimeoutOption(): void unset($client); } - public function testContextOption(): void - { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); - $client->setContext(['ssl' => ['verify_peer' => false]]); - - $this->expectWsClientConnect(context: ['ssl' => ['verify_peer' => false]]); - $this->expectWsClientPerformHandshake('localhost:8000', '/my/mock/path'); - $client->connect(); - - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); - unset($client); - } - - public function testContextClass(): void + public function testContext(): void { $this->expectContext(); $context = new Context(); diff --git a/tests/suites/server/ConfigErrorTest.php b/tests/suites/server/ConfigErrorTest.php index c81b3fd..6103024 100644 --- a/tests/suites/server/ConfigErrorTest.php +++ b/tests/suites/server/ConfigErrorTest.php @@ -28,6 +28,7 @@ public function testPortTooLow(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionCode(0); $this->expectExceptionMessage("Invalid port '-1' provided"); + // @phpstan-ignore argument.type $server = new Server(-1); } @@ -36,6 +37,7 @@ public function testPortTooHigh(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionCode(0); $this->expectExceptionMessage("Invalid port '65536' provided"); + // @phpstan-ignore argument.type $server = new Server(65536); } diff --git a/tests/suites/server/ConfigTest.php b/tests/suites/server/ConfigTest.php index 6cb1edf..1349cad 100644 --- a/tests/suites/server/ConfigTest.php +++ b/tests/suites/server/ConfigTest.php @@ -56,11 +56,10 @@ public function tearDown(): void public function testServerDefaults(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $this->assertSame($server, $server->setStreamFactory(new StreamFactory())); - $this->assertSame($server, $server->addMiddleware(new Callback())); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); + $this->assertSame($server, $server->addMiddleware(new Callback())); $this->assertEquals('WebSocket\Server(closed)', "{$server}"); $this->assertEquals(60, $server->getTimeout()); $this->assertEquals(4096, $server->getFrameSize()); @@ -73,6 +72,7 @@ public function testServerDefaults(): void $this->assertEmpty($server->getWritableConnections()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); + $this->expectStreamCollectionCount(); $this->expectStreamCollectionWaitRead()->addAssert(function ($method, $params) { $this->assertEquals(60, $params[0]); }); @@ -82,16 +82,23 @@ public function testServerDefaults(): void $server->stop(); }); $server->start(); + $this->assertEquals('WebSocket\Server(tcp://0.0.0.0:8000)', "{$server}"); $this->assertFalse($server->isRunning()); + $this->expectStreamCollectionDetach(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testServerConfiguration(): void { + $this->expectWsServerCreate(); + $server = new Server(9000, true, streamFactory: new StreamFactory()); + $this->expectStreamFactory(); - $server = new Server(9000, true); $this->assertSame($server, $server->setStreamFactory(new StreamFactory())); $this->expectWsServerSetup(scheme: 'ssl', port: 9000); @@ -138,35 +145,15 @@ public function testServerConfiguration(): void $this->expectSocketStreamIsWritable(); $this->assertCount(1, $server->getWritableConnections()); - $this->expectSocketStreamIsConnected(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); - unset($server); - } - - public function testContextOption(): void - { - $this->expectStreamFactory(); - $server = new Server(8000); - $this->assertSame($server, $server->setStreamFactory(new StreamFactory())); - $this->assertSame($server, $server->setContext(['ssl' => ['verify_peer' => false]])); - - $this->expectWsServerSetup(); - $this->expectStreamCollectionWaitRead()->addAssert(function ($method, $params) { - $this->assertEquals(60, $params[0]); - }); - $this->expectStreamCollection()->addAssert(function ($method, $params) use ($server) { - $this->assertTrue($server->isRunning()); - $this->assertEquals(0, $server->getConnectionCount()); - $server->stop(); - }); - $server->start(); - $this->assertEquals('WebSocket\Server(tcp://0.0.0.0:8000)', "{$server}"); - $this->assertFalse($server->isRunning()); + $this->expectSocketServerClose(); + $server->disconnect(); unset($server); } - public function testContextClass(): void + public function testContext(): void { $this->expectContext(); $context = new Context(); @@ -174,9 +161,9 @@ public function testContextClass(): void $this->expectContextSetOption(); $context->setOptions(['ssl' => ['verify_peer' => false]]); - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); + $server->onHandshake(function (Server $server, Connection $connection) { $this->expectSocketStreamGetContext(); $connectionContext = $connection->getContext(); @@ -208,8 +195,11 @@ public function testContextClass(): void $this->expectWsServerPerformHandshake(); $server->start(); - $this->expectSocketStreamIsConnected(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } @@ -222,14 +212,16 @@ public function testHttpFactories(): void $this->expectContextSetOption(); $context->setOptions(['ssl' => ['verify_peer' => false]]); - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->assertSame($server, $server->setResponseFactory($httpFactory)); $this->assertSame($server, $server->setServerRequestFactory($httpFactory)); $this->assertSame($server, $server->setUriFactory($httpFactory)); + $this->expectStreamCollectionDetach(); + $server->disconnect(); + unset($server); } } diff --git a/tests/suites/server/HandshakeTest.php b/tests/suites/server/HandshakeTest.php index 06fc6dc..0b40424 100644 --- a/tests/suites/server/HandshakeTest.php +++ b/tests/suites/server/HandshakeTest.php @@ -56,9 +56,8 @@ public function tearDown(): void public function testHandshakeRequest(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -78,17 +77,18 @@ public function testHandshakeRequest(): void $this->expectWsServerPerformHandshake(); $server->start(); - $this->expectSocketStreamIsConnected(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); + $this->expectSocketServerClose(); + $server->disconnect(); unset($server); } public function testHandshakeRequestVariant(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -136,17 +136,18 @@ public function testHandshakeRequestVariant(): void }); $server->start(); - $this->expectSocketStreamIsConnected(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); + $this->expectSocketServerClose(); + $server->disconnect(); unset($server); } public function testHandshakeRequestFailure(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -171,14 +172,17 @@ public function testHandshakeRequestFailure(): void $this->expectSocketStreamClose(); $server->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testHandshakeMethodFailure(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -222,14 +226,17 @@ public function testHandshakeMethodFailure(): void $this->expectSocketStreamClose(); $server->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testHandshakeConnectionHeaderFailure(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -273,14 +280,17 @@ public function testHandshakeConnectionHeaderFailure(): void $this->expectSocketStreamClose(); $server->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testHandshakeUpgradeHeaderFailure(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -324,14 +334,17 @@ public function testHandshakeUpgradeHeaderFailure(): void $this->expectSocketStreamClose(); $server->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testHandshakeVersionHeaderFailure(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -375,14 +388,17 @@ public function testHandshakeVersionHeaderFailure(): void $this->expectSocketStreamClose(); $server->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testHandshakeWebSocketKeyHeaderFailure(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -423,14 +439,17 @@ public function testHandshakeWebSocketKeyHeaderFailure(): void $this->expectSocketStreamClose(); $server->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testHandshakeWebSocketKeyInvalidFailure(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -474,14 +493,17 @@ public function testHandshakeWebSocketKeyInvalidFailure(): void $this->expectSocketStreamClose(); $server->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testHandshakeResponseFailure(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -527,6 +549,10 @@ public function testHandshakeResponseFailure(): void $this->expectSocketStreamClose(); $server->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } } diff --git a/tests/suites/server/ServerTest.php b/tests/suites/server/ServerTest.php index ef188b1..e3c8eb6 100644 --- a/tests/suites/server/ServerTest.php +++ b/tests/suites/server/ServerTest.php @@ -11,47 +11,49 @@ use Error; use PHPUnit\Framework\TestCase; -use Phrity\Net\Mock\StreamCollection; -use Phrity\Net\Mock\StreamFactory; +use Phrity\Net\Mock\{ + StreamCollection, + StreamFactory, +}; use Phrity\Net\Mock\Stack\{ ExpectContextTrait, ExpectSocketServerTrait, ExpectSocketStreamTrait, ExpectStreamCollectionTrait, - ExpectStreamFactoryTrait + ExpectStreamFactoryTrait, }; use Phrity\Net\StreamException; use Phrity\Util\ErrorHandler; -use Psr\Log\NullLogger; +use RuntimeException; use Stringable; use WebSocket\{ Connection, - Server + Server, }; use WebSocket\Exception\{ BadOpcodeException, CloseException, ConnectionClosedException, - ServerException + ServerException, }; use WebSocket\Http\{ Response, - ServerRequest + ServerRequest, }; use WebSocket\Message\{ Binary, Close, Ping, Pong, - Text + Text, }; use WebSocket\Middleware\{ Callback, - CloseHandler + CloseHandler, }; use WebSocket\Test\{ MockStreamTrait, - MockUri + MockUri, }; /** @@ -79,9 +81,9 @@ public function tearDown(): void public function testListeners(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); + $handler = new ErrorHandler(); $this->assertInstanceOf(Stringable::class, $server); $this->assertEquals('WebSocket\Server(closed)', "{$server}"); @@ -240,6 +242,7 @@ public function testListeners(): void }); $server->start(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); $this->expectSocketServerClose(); $server->disconnect(); @@ -249,9 +252,8 @@ public function testListeners(): void public function testMiddlewares(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $server->addMiddleware(new Callback()); @@ -278,17 +280,18 @@ public function testMiddlewares(): void $server->addMiddleware(new Callback()); - $this->expectSocketStreamIsConnected(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); + $this->expectSocketServerClose(); + $server->disconnect(); unset($server); } public function testBroadcastSend(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -336,17 +339,18 @@ public function testBroadcastSend(): void $message = $server->close(1000, 'Close message'); $this->assertInstanceOf(Close::class, $message); - $this->expectSocketStreamIsConnected(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); + $this->expectSocketServerClose(); + $server->disconnect(); unset($server); } public function testDetachConnection(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $server->onHandshake(function ($server, $connection, $request, $response) { $connection->disconnect(); @@ -394,14 +398,18 @@ public function testDetachConnection(): void $this->expectSocketStreamClose(); $server->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketStreamClose(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testShutdown(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $server->addMiddleware(new CloseHandler()); $server->onHandshake(function ($server, $connection, $request, $response) { @@ -498,18 +506,21 @@ public function testShutdown(): void $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectStreamCollectionDetach(); - + $this->expectStreamCollectionDetach(); $this->expectSocketServerClose(); - + $this->expectStreamCollectionCount(); $server->start(); + + $this->expectStreamCollectionDetach(); + $server->disconnect(); + unset($server); } public function testShutdownEmpty(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $server->addMiddleware(new CloseHandler()); $server->onTick(function ($server) { @@ -517,16 +528,20 @@ public function testShutdownEmpty(): void }); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections([]); + $this->expectStreamCollectionDetach(); $this->expectSocketServerClose(); - $server->start(); + + $this->expectStreamCollectionDetach(); + $server->disconnect(); + + unset($server); } public function testAlreadyStarted(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $server->onHandshake(function ($server, $connection, $request, $response) { $connection->disconnect(); @@ -554,30 +569,56 @@ public function testAlreadyStarted(): void $this->expectSocketStreamClose(); $server->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketStreamClose(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testCreateServerError(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectStreamFactoryCreateSocketServer()->addAssert(function ($method, $params) { throw new StreamException(StreamException::SERVER_SOCKET_ERR, ['uri' => 'test']); }); $this->expectException(ServerException::class); - $this->expectExceptionMessage('Server failed to start:'); + $this->expectExceptionMessage('Server failed to start: Could not create socket'); $server->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketServerClose(); + $server->disconnect(); + + unset($server); + } + + public function testCreateServerExternalError(): void + { + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); + + $this->expectStreamFactoryCreateSocketServer()->addAssert(function ($method, $params) { + throw new RuntimeException('Random exception'); + }); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Random exception'); + $server->start(); + + $this->expectStreamCollectionDetach(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testServerAccessError(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -587,14 +628,17 @@ public function testServerAccessError(): void }); $server->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testRunBadOpcodeException(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -631,6 +675,7 @@ public function testRunBadOpcodeException(): void $this->expectSocketStreamIsWritable(); $this->assertCount(1, $server->getWritableConnections()); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); $this->expectSocketServerClose(); $server->disconnect(); @@ -640,9 +685,8 @@ public function testRunBadOpcodeException(): void public function testRunConnectionClosedException(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -679,6 +723,7 @@ public function testRunConnectionClosedException(): void $this->assertEmpty($server->getReadableConnections()); $this->assertEmpty($server->getWritableConnections()); + $this->expectStreamCollectionDetach(); $this->expectSocketServerClose(); $server->disconnect(); @@ -687,9 +732,8 @@ public function testRunConnectionClosedException(): void public function testRunServerException(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -726,6 +770,7 @@ public function testRunServerException(): void $this->expectSocketStreamIsWritable(); $this->assertCount(1, $server->getWritableConnections()); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); $this->expectSocketServerClose(); $server->disconnect(); @@ -735,9 +780,8 @@ public function testRunServerException(): void public function testRunExternalException(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); $this->expectWsSelectConnections(['@server']); @@ -761,18 +805,20 @@ public function testRunExternalException(): void $server->stop(); throw new StreamException(1000); }); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); $this->expectSocketServerClose(); $this->expectException(StreamException::class); $this->expectExceptionMessage('Stream is detached.'); $server->start(); + + $server->disconnect(); } public function testUnmaskedException(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $server->onError(function ($server, $connection, $exception) { $this->assertInstanceOf(Server::class, $server); @@ -808,18 +854,20 @@ public function testUnmaskedException(): void return base64_decode('gQA='); }); $this->expectSocketStreamWrite(); - $this->expectSocketStreamIsConnected(); $server->start(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testMaxConnectionsOverflow(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $server->setMaxConnections(1); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); @@ -846,16 +894,18 @@ public function testMaxConnectionsOverflow(): void $server->start(); $this->assertEquals(1, $server->getConnectionCount()); - $this->expectSocketStreamIsConnected(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); + $this->expectSocketServerClose(); + $server->disconnect(); + unset($server); } public function testUnresolvableError(): void { - $this->expectStreamFactory(); - $server = new Server(8000); - $server->setStreamFactory(new StreamFactory()); + $this->expectWsServerCreate(); + $server = new Server(8000, streamFactory: new StreamFactory()); $server->onTick(function ($server) { /** @@ -882,6 +932,7 @@ public function testUnresolvableError(): void $this->expectSocketStreamGetRemoteName(); $this->expectSocketStreamSetTimeout(); $this->expectWsServerPerformHandshake(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); $this->expectSocketServerClose(); $this->expectException(Error::class); From d8054baaf8d8c6119b2330875768d69dfb1ca9ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Sat, 6 Sep 2025 15:00:22 +0200 Subject: [PATCH 03/16] pcov --- .github/workflows/acceptance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 4ce3c55..3533e8c 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -71,7 +71,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: "8.4" - coverage: xdebug + coverage: pcov env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Get composer cache directory From 2afef0df8bc6092375013884fa9090311d86ddfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Sun, 7 Sep 2025 18:07:31 +0200 Subject: [PATCH 04/16] Http --- composer.json | 12 +++++++++--- src/Http/DefaultHttpFactory.php | 21 +++++++++++++++++---- src/Server.php | 24 ++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 3a83665..5513c49 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ }, "require": { "php": "^8.1", + "phrity/http": "dev-main", "phrity/net-uri": "^2.1", "phrity/net-stream": "dev-ws4 as 2.4.0", "psr/http-message": "^1.1 | ^2.0", @@ -35,7 +36,6 @@ }, "require-dev": { "guzzlehttp/psr7": "^2.0", - "php-coveralls/php-coveralls": "^2.0", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.0 | ^11.0 | ^12.0", "phrity/logger-console": "^1.0", @@ -46,7 +46,13 @@ }, "suggest": { "ext-zlib": "Required for per-message deflate compression", - "psr/http-message-implementation": "Use a complete http-message implementation", + "psr/http-factory-implementation": "Use a complete http-factory implementation", "psr/log-implementation": "Use a logger implementation" - } + }, + "repositories": [ + { + "type": "path", + "url": "../phrity-http" + } + ] } diff --git a/src/Http/DefaultHttpFactory.php b/src/Http/DefaultHttpFactory.php index 0e99f87..fd0e5c3 100644 --- a/src/Http/DefaultHttpFactory.php +++ b/src/Http/DefaultHttpFactory.php @@ -7,6 +7,7 @@ namespace WebSocket\Http; +use Phrity\Http\HttpFactory; use Phrity\Net\UriFactory; use Psr\Http\Message\{ RequestFactoryInterface, @@ -23,11 +24,15 @@ * WebSocket\Http\DefaultHttpFactory * Only used for handshake procedure. */ -class DefaultHttpFactory extends UriFactory implements - RequestFactoryInterface, - ResponseFactoryInterface, - ServerRequestFactoryInterface +class DefaultHttpFactory extends HttpFactory { + private UriFactory $uriFactory; + + public function __construct() + { + $this->uriFactory = new UriFactory(); + } + /** * Create a new request. * @param string $method @@ -56,4 +61,12 @@ public function createServerRequest(string $method, mixed $uri, array $serverPar { return new ServerRequest($method, $uri); } + + /** + * @param string $uri The URI to parse. + */ + public function createUri(string $uri = ''): UriInterface + { + return $this->uriFactory->createUri($uri); + } } diff --git a/src/Server.php b/src/Server.php index 9b97612..8dc8e99 100644 --- a/src/Server.php +++ b/src/Server.php @@ -8,6 +8,7 @@ namespace WebSocket; use InvalidArgumentException; +use Phrity\Http\HttpFactory; use Phrity\Net\{ Context, SocketServer, @@ -89,21 +90,27 @@ class Server implements LoggerAwareInterface, Stringable private UriFactoryInterface $uriFactory; private StreamFactory $streamFactory; + private HttpFactory $httpFactory; private Watcher $watcher; + /* ---------- Magic methods ------------------------------------------------------------------------------------ */ /** * @param int<0, 65535> $port Socket port to listen to * @param bool $ssl If SSL should be used * @param StreamFactory|null $streamFactory + * @param HttpFactory|null $httpFactory * @param Watcher|null $watcher * @throws InvalidArgumentException If invalid port provided */ public function __construct( int $port = 80, bool $ssl = false, + LoggerInterface|null $logger = null, + Context|null $context = null, StreamFactory|null $streamFactory = null, + HttpFactory|null $httpFactory = null, Watcher|null $watcher = null, ) { if ($port < 0 || $port > 65535) { @@ -111,11 +118,13 @@ public function __construct( } $this->port = $port; $this->scheme = $ssl ? 'ssl' : 'tcp'; - $this->initLogger(); - $this->context = new Context(); + $this->initLogger($logger); + $this->context = $context ?? new Context(); +// @todo Get rid off $this->responseFactory = $this->serverRequestFactory = $this->uriFactory = new DefaultHttpFactory(); $this->streamFactory = $streamFactory ?? new StreamFactory(); + $this->httpFactory = $httpFactory ?? new DefaultHttpFactory(); $this->watcher = $watcher ?? new Watcher($this->streamFactory->createStreamCollection()); } @@ -142,6 +151,17 @@ public function setStreamFactory(StreamFactory $streamFactory): self return $this; } + /** + * Set HTTP factory to use. + * @param HttpFactory $httpFactory + * @return self + */ + public function setHttpFactory(HttpFactory $httpFactory): self + { + $this->httpFactory = $httpFactory; + return $this; + } + /** * Set logger. * @param LoggerInterface $logger Logger implementation From 105e2c1616ea508c1befef127f1fadffbd8f0214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Sun, 14 Sep 2025 15:08:20 +0200 Subject: [PATCH 05/16] watcher, step 1 --- composer.json | 10 +- docs/Client.md | 21 ++- docs/Server.md | 21 ++- src/Client.php | 146 ++++++++-------- src/Connection.php | 27 +-- src/Http/HttpHandler.php | 19 +-- src/Server.php | 88 +++------- tests/mock/MockStreamTrait.php | 9 +- tests/suites/client/ClientErrorTest.php | 44 ++--- tests/suites/client/ClientTest.php | 158 +++++++++++------- tests/suites/client/ConfigTest.php | 132 +++++++++------ tests/suites/client/HandshakeTest.php | 61 +++---- tests/suites/connection/ConnectionTest.php | 30 ---- tests/suites/connection/ExceptionTest.php | 22 --- tests/suites/middleware/CallbackTest.php | 10 -- .../middleware/DeflateCompressorTest.php | 14 -- .../suites/middleware/FollowRedirectTest.php | 7 +- tests/suites/middleware/PingIntervalTest.php | 2 - tests/suites/middleware/ProcessStackTest.php | 4 - .../middleware/SubprotocolNegotiationTest.php | 12 -- tests/suites/server/ConfigTest.php | 9 +- 21 files changed, 370 insertions(+), 476 deletions(-) diff --git a/composer.json b/composer.json index 5513c49..80fac80 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ }, "require": { "php": "^8.1", - "phrity/http": "dev-main", + "phrity/http": "^1.0", "phrity/net-uri": "^2.1", "phrity/net-stream": "dev-ws4 as 2.4.0", "psr/http-message": "^1.1 | ^2.0", @@ -48,11 +48,5 @@ "ext-zlib": "Required for per-message deflate compression", "psr/http-factory-implementation": "Use a complete http-factory implementation", "psr/log-implementation": "Use a logger implementation" - }, - "repositories": [ - { - "type": "path", - "url": "../phrity-http" - } - ] + } } diff --git a/docs/Client.md b/docs/Client.md index 66c7971..e91b595 100644 --- a/docs/Client.md +++ b/docs/Client.md @@ -201,13 +201,22 @@ $client->getContext(); // => currently used Phrity\Net\Context ### HTTP factories By default the Client uses a minimal [PSR-7 HTTP message](https://www.php-fig.org/psr/psr-7/) implementation. -To use complete implementations such as `nyholm/psr7` or `guzzlehttp/psr7`, add their [PSR-17 HTTP factories](https://www.php-fig.org/psr/psr-17/) during setup. +Other (more complete) implementations can be used by setting [PSR-17 HTTP factories](https://www.php-fig.org/psr/psr-17/) on the CLient. + +Set a configured HttpFactory class on the Client. ```php -$httpFactory = new Nyholm\Psr7\Factory\Psr17Factory(); // Or any other PSR-17 factory -$client - ->setResponseFactory($httpFactory) - ->setRequestFactory($httpFactory) - ->setUriFactory($httpFactory); +$factory = new Phrity\Http\HttpFactory( + requestFactory: $myRequestFactory, + responseFactory: $myResponseFactory, + uriFactory: $myUriFactory, +); +$client->setHttpFactory($factory); + +``` +Or if you use factories that support multiple interfaces, available in libraries such as `nyholm/psr7` or `guzzlehttp/psr7`. +```php +$psrFactory = new Nyholm\Psr7\Factory\Psr17Factory(); // Or any other PSR-17 factory +$client->setHttpFactory(Phrity\Http\HttpFactory::create($psrFactory)); ``` ### Handshake headers diff --git a/docs/Server.md b/docs/Server.md index 6e66e83..f044918 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -192,13 +192,22 @@ $server->getContext(); // => currently used Phrity\Net\Context ### HTTP factories By default the Server uses a minimal [PSR-7 HTTP message](https://www.php-fig.org/psr/psr-7/) implementation. -To use complete implementations such as `nyholm/psr7` or `guzzlehttp/psr7`, add their [PSR-17 HTTP factories](https://www.php-fig.org/psr/psr-17/) during setup. +Other (more complete) implementations can be used by setting [PSR-17 HTTP factories](https://www.php-fig.org/psr/psr-17/) on the Server. + +Set a configured HttpFactory class on the Client. ```php -$httpFactory = new Nyholm\Psr7\Factory\Psr17Factory(); // Or any other PSR-17 factory -$server - ->setResponseFactory($httpFactory) - ->setServerRequestFactory($httpFactory) - ->setUriFactory($httpFactory); +$factory = new Phrity\Http\HttpFactory( + serverRequestFactory: $myServerRequestFactory, + responseFactory: $myResponseFactory, + uriFactory: $myUriFactory, +); +$server->setHttpFactory($factory); +``` + +Or if you use factories that support multiple interfaces, available in libraries such as `nyholm/psr7` or `guzzlehttp/psr7`. +```php +$psrFactory = new Nyholm\Psr7\Factory\Psr17Factory(); // Or any other PSR-17 factory +$server->setHttpFactory(Phrity\Http\HttpFactory::create($psrFactory)); ``` ### Max connections diff --git a/src/Client.php b/src/Client.php index 6ce3d61..f4effb7 100644 --- a/src/Client.php +++ b/src/Client.php @@ -8,20 +8,18 @@ namespace WebSocket; use InvalidArgumentException; +use Phrity\Http\HttpFactory; use Phrity\Net\{ Context, + SocketStream, StreamCollection, StreamFactory, Uri }; use Psr\Http\Message\{ - RequestFactoryInterface, RequestInterface, - ResponseFactoryInterface, ResponseInterface, - ServerRequestFactoryInterface, UriInterface, - UriFactoryInterface, }; use Psr\Log\{ LoggerAwareInterface, @@ -48,6 +46,7 @@ SendMethodsTrait, StringableTrait }; +use WebSocket\Runtime\Watcher; /** * WebSocket\Client class. @@ -72,36 +71,41 @@ class Client implements LoggerAwareInterface, Stringable private array $headers = []; // Internal resources - private StreamFactory $streamFactory; private Uri $socketUri; private Connection|null $connection = null; /** @var array $middlewares */ private array $middlewares = []; - private StreamCollection|null $streams = null; private bool $running = false; - private RequestFactoryInterface $requestFactory; - private ResponseFactoryInterface $responseFactory; - private ServerRequestFactoryInterface $serverRequestFactory; - private UriFactoryInterface $uriFactory; + private StreamFactory $streamFactory; + private HttpFactory $httpFactory; + private Watcher $watcher; /* ---------- Magic methods ------------------------------------------------------------------------------------ */ /** * @param UriInterface|string $uri A ws/wss-URI - */ - public function __construct(UriInterface|string $uri) - { + * @param LoggerInterface|null $logger + * @param Context|null $context $logger + * @param StreamFactory|null $streamFactory + * @param HttpFactory|null $httpFactory + * @param Watcher|null $watcher + */ + public function __construct( + UriInterface|string $uri, + LoggerInterface|null $logger = null, + Context|null $context = null, + StreamFactory|null $streamFactory = null, + HttpFactory|null $httpFactory = null, + Watcher|null $watcher = null, + ) { $this->socketUri = $this->parseUri($uri); - $this->initLogger(); - $this->context = new Context(); - $this->setStreamFactory(new StreamFactory()); - $this->requestFactory - = $this->responseFactory - = $this->serverRequestFactory - = $this->uriFactory - = new DefaultHttpFactory(); + $this->initLogger($logger); + $this->context = $context ?? new Context(); + $this->streamFactory = $streamFactory ?? new StreamFactory(); + $this->httpFactory = $httpFactory ?? new DefaultHttpFactory(); + $this->watcher = $watcher ?? new Watcher($this->streamFactory->createStreamCollection()); } /** @@ -127,6 +131,17 @@ public function setStreamFactory(StreamFactory $streamFactory): self return $this; } + /** + * Set HTTP factory to use. + * @param HttpFactory $httpFactory + * @return self + */ + public function setHttpFactory(HttpFactory $httpFactory): self + { + $this->httpFactory = $httpFactory; + return $this; + } + /** * Set logger. * @param LoggerInterface $logger Logger implementation @@ -139,36 +154,6 @@ public function setLogger(LoggerInterface $logger): void } } - /** - * Set ResponseFactory. - * @param ResponseFactoryInterface $responseFactory ResponseFactory to use - */ - public function setResponseFactory(ResponseFactoryInterface $responseFactory): self - { - $this->responseFactory = $responseFactory; - return $this; - } - - /** - * Set RequestFactory. - * @param RequestFactoryInterface $requestFactory RequestFactory to use - */ - public function setRequestFactory(RequestFactoryInterface $requestFactory): self - { - $this->requestFactory = $requestFactory; - return $this; - } - - /** - * Set UriFactory. - * @param UriFactoryInterface $uriFactory UriFactory to use - */ - public function setUriFactory(UriFactoryInterface $uriFactory): self - { - $this->uriFactory = $uriFactory; - return $this; - } - /** * Set timeout. * @param int<0, max>|float $timeout Timeout in seconds @@ -328,27 +313,8 @@ public function start(int|float|null $timeout = null): void // Run handler while ($this->running) { - /** @var StreamCollection */ - $streams = $this->streams; try { - // Get streams with readable content - $readables = $streams->waitRead($timeout ?? $this->timeout); - foreach ($readables as $key => $readable) { - try { - // Read from connection - $message = $connection->pullMessage(); - $this->dispatch($message->getOpcode(), [$this, $connection, $message]); - } catch (MessageLevelInterface $e) { - // Error, but keep connection open - $this->logger->error("[client] {$e->getMessage()}", ['exception' => $e]); - $this->dispatch('error', [$this, $connection, $e]); - } catch (ConnectionLevelInterface $e) { - // Error, disconnect connection - $this->disconnect(); - $this->logger->error("[client] {$e->getMessage()}", ['exception' => $e]); - $this->dispatch('error', [$this, $connection, $e]); - } - } + $this->watcher->watch($timeout ?? $this->timeout); if (!$connection->isConnected()) { $this->running = false; } @@ -392,6 +358,27 @@ public function start(int|float|null $timeout = null): void } } + private function selectHandler(string $key, SocketStream $stream): void + { + /** @var Connection $connection */ + $connection = $this->connection; + try { + // Read from connection + $message = $connection->pullMessage(); + $this->dispatch($message->getOpcode(), [$this, $connection, $message]); + } catch (MessageLevelInterface $e) { + // Error, but keep connection open + $this->logger->error("[client] {$e->getMessage()}", ['exception' => $e]); + $this->dispatch('error', [$this, $connection, $e]); + } catch (ConnectionLevelInterface $e) { + // Error, disconnect connection + $this->disconnect(); + $this->logger->error("[client] {$e->getMessage()}", ['exception' => $e]); + $this->dispatch('error', [$this, $connection, $e]); + } + } + + /** * Stop client listener (resumable). */ @@ -448,7 +435,6 @@ public function isWritable(): bool public function connect(): void { $this->disconnect(); - $this->streams = $this->streamFactory->createStreamCollection(); $hostUri = (new Uri()) ->withScheme(match ($this->socketUri->getScheme()) { @@ -471,16 +457,16 @@ public function connect(): void $this->logger->error("[client] {$error}", ['exception' => $e]); throw new ClientException($error); } - $name = $stream->getRemoteName(); - $this->streams->attach($stream, $name); + $name = $stream->getRemoteName() ?? 'unknown'; + $this->watcher->attach($name, $stream, function (string $key, SocketStream $stream) { + $this->selectHandler($key, $stream); + }); $this->connection = new Connection( $stream, true, false, $hostUri->getScheme() === 'ssl', - $this->responseFactory, - $this->serverRequestFactory, - $this->uriFactory + $this->httpFactory ); $this->connection->setFrameSize($this->frameSize); $this->connection->setTimeout($this->timeout); @@ -492,6 +478,7 @@ public function connect(): void if (!$this->isConnected()) { $error = "Invalid stream on \"{$hostUri}\"."; $this->logger->error("[client] {$error}"); + $this->disconnect(); throw new ClientException($error); } try { @@ -522,6 +509,9 @@ public function connect(): void */ public function disconnect(): void { + if ($this->connection) { + $this->watcher->detach($this->connection->getRemoteName()); + } if ($this->connection && $this->isConnected()) { $this->connection->disconnect(); $this->logger->info('[client] Client disconnected'); @@ -571,7 +561,7 @@ protected function performHandshake(Uri $uri, Connection $connection): ResponseI // Generate the WebSocket key. $key = $this->generateKey(); - $request = $this->requestFactory->createRequest('GET', $uri); + $request = $this->httpFactory->createRequest('GET', $uri); $request = $request ->withHeader('User-Agent', 'websocket-client-php') diff --git a/src/Connection.php b/src/Connection.php index 19ab087..81453a4 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -8,6 +8,7 @@ namespace WebSocket; use InvalidArgumentException; +use Phrity\Http\HttpFactory; use Phrity\Net\{ Context, SocketStream, @@ -15,10 +16,7 @@ use Psr\Http\Message\{ MessageInterface, RequestInterface, - ResponseFactoryInterface, ResponseInterface, - ServerRequestFactoryInterface, - UriFactoryInterface, }; use Psr\Log\{ LoggerAwareInterface, @@ -73,7 +71,6 @@ class Connection implements LoggerAwareInterface, Stringable private ResponseInterface|null $handshakeResponse = null; /** @var array $meta */ private array $meta = []; - private bool $closed = false; /* ---------- Magic methods ------------------------------------------------------------------------------------ */ @@ -83,12 +80,10 @@ public function __construct( bool $pushMasked, bool $pullMaskedRequired, bool $ssl = false, - ResponseFactoryInterface|null $responseFactory = null, - ServerRequestFactoryInterface|null $serverRequestFactory = null, - UriFactoryInterface|null $uriFactory = null, + HttpFactory|null $httpFactory = null ) { $this->stream = $stream; - $this->httpHandler = new HttpHandler($this->stream, $ssl, $responseFactory, $serverRequestFactory, $uriFactory); + $this->httpHandler = new HttpHandler($this->stream, $ssl, $httpFactory); $this->messageHandler = new MessageHandler(new FrameHandler($this->stream, $pushMasked, $pullMaskedRequired)); $this->middlewareHandler = new MiddlewareHandler($this->messageHandler, $this->httpHandler); $this->localName = $this->stream->getLocalName() ?? ''; @@ -96,13 +91,6 @@ public function __construct( $this->initLogger(); } - public function __destruct() - { - if (!$this->closed && $this->isConnected()) { - $this->stream->close(); - } - } - public function __toString(): string { return $this->stringable('%s:%s', $this->localName, $this->remoteName); @@ -232,7 +220,6 @@ public function disconnect(): self { $this->logger->info('[connection] Closing connection'); $this->stream->close(); - $this->closed = true; return $this; } @@ -263,18 +250,18 @@ public function closeWrite(): self /** * Get name of local socket, or null if not connected. - * @return string|null + * @return string */ - public function getName(): string|null + public function getName(): string { return $this->localName; } /** * Get name of remote socket, or null if not connected. - * @return string|null + * @return string */ - public function getRemoteName(): string|null + public function getRemoteName(): string { return $this->remoteName; } diff --git a/src/Http/HttpHandler.php b/src/Http/HttpHandler.php index bf9b453..a5b3255 100644 --- a/src/Http/HttpHandler.php +++ b/src/Http/HttpHandler.php @@ -7,6 +7,7 @@ namespace WebSocket\Http; +use Phrity\Http\HttpFactory; use Phrity\Net\{ SocketStream, Uri @@ -31,22 +32,16 @@ class HttpHandler implements Stringable private SocketStream $stream; private bool $ssl; - private ResponseFactoryInterface $responseFactory; - private ServerRequestFactoryInterface $serverRequestFactory; - private UriFactoryInterface $uriFactory; + private HttpFactory $httpFactory; public function __construct( SocketStream $stream, bool $ssl = false, - ResponseFactoryInterface|null $responseFactory = null, - ServerRequestFactoryInterface|null $serverRequestFactory = null, - UriFactoryInterface|null $uriFactory = null, + HttpFactory|null $httpFactory = null ) { $this->stream = $stream; $this->ssl = $ssl; - $this->responseFactory = $responseFactory ?? new DefaultHttpFactory(); - $this->serverRequestFactory = $serverRequestFactory ?? new DefaultHttpFactory(); - $this->uriFactory = $uriFactory ?? new DefaultHttpFactory(); + $this->httpFactory = $httpFactory ?? new DefaultHttpFactory(); } /** @@ -60,7 +55,7 @@ public function pull(): MessageInterface // Pulling server request preg_match('!^(?P[A-Z]+) (?P[^ ]*) HTTP/(?P[0-9/.]+)!', $status, $matches); if (!empty($matches)) { - $message = $this->serverRequestFactory->createServerRequest($matches['method'], ''); + $message = $this->httpFactory->createServerRequest($matches['method'], ''); $path = $matches['path']; $version = $matches['version']; } @@ -68,7 +63,7 @@ public function pull(): MessageInterface // Pulling response preg_match('!^HTTP/(?P[0-9/.]+) (?P[0-9]*)($|\s(?P.*))!', $status, $matches); if (!empty($matches)) { - $message = $this->responseFactory->createResponse((int)$matches['code'], $matches['reason'] ?? ''); + $message = $this->httpFactory->createResponse((int)$matches['code'], $matches['reason'] ?? ''); $version = $matches['version']; } @@ -92,7 +87,7 @@ public function pull(): MessageInterface } if ($message instanceof Request) { $scheme = $this->ssl ? 'wss' : 'ws'; - $uri = $this->uriFactory->createUri("{$scheme}://{$message->getHeaderLine('Host')}{$path}"); + $uri = $this->httpFactory->createUri("{$scheme}://{$message->getHeaderLine('Host')}{$path}"); $message = $message->withUri($uri, true); } diff --git a/src/Server.php b/src/Server.php index 8dc8e99..819e5c6 100644 --- a/src/Server.php +++ b/src/Server.php @@ -18,13 +18,6 @@ StreamFactory, Uri }; -use Psr\Http\Message\{ - ResponseFactoryInterface, - ResponseInterface, - ServerRequestFactoryInterface, - UriInterface, - UriFactoryInterface, -}; use Psr\Log\{ LoggerAwareInterface, LoggerInterface, @@ -85,10 +78,6 @@ class Server implements LoggerAwareInterface, Stringable private array $middlewares = []; private int|null $maxConnections = null; - private ResponseFactoryInterface $responseFactory; - private ServerRequestFactoryInterface $serverRequestFactory; - private UriFactoryInterface $uriFactory; - private StreamFactory $streamFactory; private HttpFactory $httpFactory; private Watcher $watcher; @@ -99,6 +88,8 @@ class Server implements LoggerAwareInterface, Stringable /** * @param int<0, 65535> $port Socket port to listen to * @param bool $ssl If SSL should be used + * @param LoggerInterface|null $logger + * @param Context|null $context $logger * @param StreamFactory|null $streamFactory * @param HttpFactory|null $httpFactory * @param Watcher|null $watcher @@ -120,9 +111,6 @@ public function __construct( $this->scheme = $ssl ? 'ssl' : 'tcp'; $this->initLogger($logger); $this->context = $context ?? new Context(); -// @todo Get rid off - $this->responseFactory = $this->serverRequestFactory = $this->uriFactory = new DefaultHttpFactory(); - $this->streamFactory = $streamFactory ?? new StreamFactory(); $this->httpFactory = $httpFactory ?? new DefaultHttpFactory(); $this->watcher = $watcher ?? new Watcher($this->streamFactory->createStreamCollection()); @@ -174,36 +162,6 @@ public function setLogger(LoggerInterface $logger): void } } - /** - * Set ResponseFactory. - * @param ResponseFactoryInterface $responseFactory ResponseFactory to use - */ - public function setResponseFactory(ResponseFactoryInterface $responseFactory): self - { - $this->responseFactory = $responseFactory; - return $this; - } - - /** - * Set ServerRequestFactory. - * @param ServerRequestFactoryInterface $serverRequestFactory ServerRequestFactory to use - */ - public function setServerRequestFactory(ServerRequestFactoryInterface $serverRequestFactory): self - { - $this->serverRequestFactory = $serverRequestFactory; - return $this; - } - - /** - * Set UriFactory. - * @param UriFactoryInterface $uriFactory UriFactory to use - */ - public function setUriFactory(UriFactoryInterface $uriFactory): self - { - $this->uriFactory = $uriFactory; - return $this; - } - /** * Set timeout. * @param int<0, max>|float $timeout Timeout in seconds @@ -443,6 +401,24 @@ public function start(int|float|null $timeout = null): void } } + /** + * Stop server listener (resumable). + */ + public function stop(): void + { + $this->running = false; + $this->logger->info("[server] Server is stopped"); + } + + /** + * If server is running (accepting connections and messages). + * @return bool + */ + public function isRunning(): bool + { + return $this->running; + } + private function selectHandler(string $key, SocketStream $stream): void { try { @@ -473,24 +449,6 @@ private function selectHandler(string $key, SocketStream $stream): void } } - /** - * Stop server listener (resumable). - */ - public function stop(): void - { - $this->running = false; - $this->logger->info("[server] Server is stopped"); - } - - /** - * If server is running (accepting connections and messages). - * @return bool - */ - public function isRunning(): bool - { - return $this->running; - } - /* ---------- Connection management ---------------------------------------------------------------------------- */ @@ -588,9 +546,7 @@ protected function acceptHandler(SocketServer $socket): void false, true, $this->isSsl(), - $this->responseFactory, - $this->serverRequestFactory, - $this->uriFactory, + $this->httpFactory, ); } catch (StreamException $e) { throw new ConnectionFailureException("Server failed to accept: {$e->getMessage()}"); @@ -637,7 +593,7 @@ protected function detachUnconnected(): void // Perform upgrade handshake on new connections. protected function performHandshake(Connection $connection): ServerRequest { - $response = $this->responseFactory->createResponse(101); + $response = $this->httpFactory->createResponse(101); $exception = null; // Read handshake request diff --git a/tests/mock/MockStreamTrait.php b/tests/mock/MockStreamTrait.php index f4aa933..8b9cae7 100644 --- a/tests/mock/MockStreamTrait.php +++ b/tests/mock/MockStreamTrait.php @@ -38,6 +38,13 @@ trait MockStreamTrait /* ---------- WebSocket Client combinded asserts --------------------------------------------------------------- */ + private function expectWsClientCreate(): void + { + $this->expectStreamFactory(); + $this->expectStreamFactoryCreateStreamCollection(); + $this->expectStreamCollection(); + } + /** * @param array $context */ @@ -49,8 +56,6 @@ private function expectWsClientSetup( array $context = [], bool $persistent = false, ): void { - $this->expectStreamFactoryCreateStreamCollection(); - $this->expectStreamCollection(); $this->expectStreamFactoryCreateSocketClient()->addAssert( function ($method, $params) use ($scheme, $host, $port) { $this->assertInstanceOf('Phrity\Net\Uri', $params[0]); diff --git a/tests/suites/client/ClientErrorTest.php b/tests/suites/client/ClientErrorTest.php index f308977..6908016 100644 --- a/tests/suites/client/ClientErrorTest.php +++ b/tests/suites/client/ClientErrorTest.php @@ -56,9 +56,8 @@ public function tearDown(): void public function testFailedSocket(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $this->expectWsClientSetup(); $this->expectSocketClientConnect()->setReturn(function () { @@ -73,9 +72,8 @@ public function testFailedSocket(): void public function testFailedConnection(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $this->expectWsClientSetup(); $this->expectSocketClientConnect(); @@ -90,6 +88,7 @@ public function testFailedConnection(): void $this->expectSocketStreamIsConnected()->setReturn(function () { return false; }); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected()->setReturn(function () { return false; }); @@ -102,9 +101,8 @@ public function testFailedConnection(): void public function testReceiveBadOpcode(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); @@ -119,8 +117,7 @@ public function testReceiveBadOpcode(): void $this->expectSocketStreamRead()->setReturn(function () { return 'Test message'; }); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); + $this->expectException(BadOpcodeException::class); $this->expectExceptionMessage("Invalid opcode '15' provided"); $message = $client->receive(); @@ -130,9 +127,8 @@ public function testReceiveBadOpcode(): void public function testBrokenWrite(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); @@ -146,10 +142,10 @@ public function testBrokenWrite(): void $this->expectSocketStreamGetMetadata()->setReturn(function () { return ['eof' => true, 'mode' => 'rw', 'seekable' => false]; }); +// $this->expectSocketStreamClose(); $this->expectException(ConnectionClosedException::class); $this->expectExceptionMessage('Connection has unexpectedly closed'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); + $client->text('Failing to write'); unset($client); @@ -157,9 +153,8 @@ public function testBrokenWrite(): void public function testReadTimeout(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); @@ -173,10 +168,9 @@ public function testReadTimeout(): void $this->expectSocketStreamGetMetadata()->setReturn(function () { return ['timed_out' => true, 'mode' => 'rw', 'seekable' => false]; }); +// $this->expectSocketStreamClose(); $this->expectException(ConnectionTimeoutException::class); $this->expectExceptionMessage('Connection operation timeout'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $client->receive(); unset($client); @@ -184,9 +178,8 @@ public function testReadTimeout(): void public function testEmptyRead(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); @@ -200,10 +193,9 @@ public function testEmptyRead(): void $this->expectSocketStreamGetMetadata()->setReturn(function () { return ['timed_out' => true, 'mode' => 'rw', 'seekable' => false]; }); +// $this->expectSocketStreamClose(); $this->expectException(ConnectionTimeoutException::class); $this->expectExceptionMessage('Connection operation timeout'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $client->receive(); $this->expectSocketStreamClose(); diff --git a/tests/suites/client/ClientTest.php b/tests/suites/client/ClientTest.php index bbae0e1..ff66171 100644 --- a/tests/suites/client/ClientTest.php +++ b/tests/suites/client/ClientTest.php @@ -78,9 +78,8 @@ public function tearDown(): void public function testClientSendReceive(): void { // Creating client - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $this->assertInstanceOf(Stringable::class, $client); $this->assertFalse($client->isConnected()); @@ -128,17 +127,19 @@ public function testClientSendReceive(): void $this->expectSocketStreamIsConnected(); $this->assertTrue($client->isConnected()); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testSendMessages(): void { // Creating client - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); @@ -154,16 +155,18 @@ public function testSendMessages(): void }); $client->send(new Text('Sending a message')); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testPayload128(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $client->setFrameSize(65540); $this->expectWsClientConnect(); @@ -201,16 +204,19 @@ public function testPayload128(): void $this->assertEquals($payload, $message->getContent()); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testPayload65536(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); + $client->setFrameSize(65540); $this->expectWsClientConnect(); @@ -284,16 +290,18 @@ public function testPayload65536(): void $this->assertEquals($payload, $message->getContent()); $this->assertEquals(65540, $client->getFrameSize()); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } - public function testMultiFrame(): void + public function stestMultiFrame(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); @@ -369,16 +377,19 @@ public function testMultiFrame(): void $this->assertEquals('Multi fragment test', $message->getContent()); $this->assertEquals(8, $client->getFrameSize()); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testPingPong(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); + $client->addMiddleware(new PingResponder()); $this->expectWsClientConnect(); @@ -476,16 +487,19 @@ public function testPingPong(): void $message = $client->receive(); $this->assertEquals('Receiving a message', $message->getContent()); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testRemoteClose(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); + $client->addMiddleware(new CloseHandler()); $this->expectWsClientConnect(); @@ -525,9 +539,9 @@ public function testRemoteClose(): void public function testAutoConnect(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); + $client->addMiddleware(new CloseHandler()); $this->expectWsClientConnect(); @@ -563,8 +577,8 @@ public function testAutoConnect(): void $this->assertFalse($client->isConnected()); // Implicit reconnect and handshake, receive message + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); - $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { @@ -587,16 +601,19 @@ public function testAutoConnect(): void $this->expectSocketStreamIsConnected(); $this->assertTrue($client->isConnected()); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testFrameFragmentation(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); + $client->addMiddleware(new CloseHandler()); $client->addMiddleware(new PingResponder()); @@ -713,9 +730,9 @@ public function testFrameFragmentation(): void public function testConvenienceMethods(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); + $client->addMiddleware(new CloseHandler()); $client->addMiddleware(new PingResponder()); @@ -776,38 +793,39 @@ public function testConvenienceMethods(): void $this->expectSocketStreamIsConnected(); $this->assertEquals('localhost:8000', $client->getRemoteName()); - - $this->expectSocketStreamIsConnected(); $this->assertEquals('WebSocket\Client(ws://localhost:8000/my/mock/path)', "{$client}"); + $this->expectStreamCollectionDetach(); + $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testDisconnect(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); $client->connect(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); - $this->expectSocketStreamIsConnected(); $client->disconnect(); + $this->expectStreamCollectionDetach(); + $this->expectSocketStreamIsConnected(); $client->disconnect(); } public function testListeners(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); - $handler = new ErrorHandler(); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $client->onHandshake(function ($client, $connection, $request, $response) { $this->assertInstanceOf(Client::class, $client); @@ -937,16 +955,18 @@ public function testListeners(): void $this->expectSocketStreamIsConnected(); $client->start(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testAlreadyStarted(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $client->onHandshake(function ($client, $connection, $request, $response) { $client->start(); @@ -954,18 +974,20 @@ public function testAlreadyStarted(): void }); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectSocketStreamIsConnected(); $client->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testRunConnectionClosedException(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $client->onText(function ($client, $connection, $message) { $this->assertInstanceOf(Client::class, $client); @@ -982,6 +1004,7 @@ public function testRunConnectionClosedException(): void })->setReturn(function () { throw new ConnectionClosedException(); }); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); $this->expectSocketStreamIsConnected(); @@ -992,9 +1015,8 @@ public function testRunConnectionClosedException(): void public function testRunClientException(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $client->onText(function ($client, $connection, $message) { $this->assertInstanceOf(Client::class, $client); @@ -1011,23 +1033,25 @@ public function testRunClientException(): void })->setReturn(function () { throw new ClientException(); }); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); $client->start(); + unset($client); } public function testRunExternalException(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); $this->expectWsClientSelectConnections(['localhost:8000'])->setReturn(function () { throw new StreamException(1000); }); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); $this->expectException(StreamException::class); @@ -1039,9 +1063,9 @@ public function testRunExternalException(): void public function testCloseException(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); + $client->addMiddleware(new CloseHandler()); $client->onText(function ($client, $connection, $message) { throw new CloseException(); @@ -1077,9 +1101,9 @@ public function testCloseException(): void public function testReconnectException(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); + $client->addMiddleware(new CloseHandler()); $client->onText(function ($client, $connection, $message) { throw new ReconnectException(new Uri('ws://localhost:8000/my/mock/path')); @@ -1113,18 +1137,22 @@ public function testReconnectException(): void $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('ggA='); // binary }); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamIsConnected(); $client->start(); + $this->expectStreamCollectionDetach(); + $this->expectSocketStreamIsConnected(); + $client->disconnect(); + unset($client); } public function testUnresolvableError(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); $client->onTick(function ($client) { /** @@ -1141,11 +1169,13 @@ public function testUnresolvableError(): void return base64_decode('gQA='); }); $this->expectSocketStreamIsConnected(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); $this->expectException(Error::class); $this->expectExceptionMessage('Class "WebSocket\Test\Client\UnexistingClass" not found'); $client->start(); + unset($client); } } diff --git a/tests/suites/client/ConfigTest.php b/tests/suites/client/ConfigTest.php index 3a69db9..4a6867c 100644 --- a/tests/suites/client/ConfigTest.php +++ b/tests/suites/client/ConfigTest.php @@ -9,9 +9,10 @@ namespace WebSocket\Test\Client; -use GuzzleHttp\Psr7\HttpFactory; +use GuzzleHttp\Psr7\HttpFactory as GuzzleFactory; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Phrity\Http\HttpFactory; use Phrity\Net\Mock\{ Context, StreamFactory, @@ -58,31 +59,38 @@ public function tearDown(): void public function testUriStringExtended(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path?my_query=yes#my_fragment'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client( + 'ws://localhost:8000/my/mock/path?my_query=yes#my_fragment', + streamFactory: new StreamFactory() + ); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake('localhost:8000', '/my/mock/path?my_query=yes'); $client->connect(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testUriStringWithoutPath(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000', streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake('localhost:8000', '/'); $client->connect(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } @@ -91,16 +99,18 @@ public function testUriInstanceRelativePath(): void $uri = new Uri('ws://localhost:8000'); $uri = $uri->withPath('my/mock/path'); - $this->expectStreamFactory(); - $client = new Client($uri); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client($uri, streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake('localhost:8000', '/my/mock/path'); $client->connect(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } @@ -109,16 +119,18 @@ public function testUriInstanceWsDefaultPort(): void $uri = new Uri('ws://localhost'); $uri = $uri->withPath('my/mock/path'); - $this->expectStreamFactory(); - $client = new Client($uri); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client($uri, streamFactory: new StreamFactory()); $this->expectWsClientConnect(port: 80); $this->expectWsClientPerformHandshake('localhost', '/my/mock/path'); $client->connect(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } @@ -127,16 +139,18 @@ public function testUriInstanceWssDefaultPort(): void $uri = new Uri('wss://localhost'); $uri = $uri->withPath('my/mock/path'); - $this->expectStreamFactory(); - $client = new Client($uri); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client($uri, streamFactory: new StreamFactory()); $this->expectWsClientConnect(scheme: 'ssl', port: 443); $this->expectWsClientPerformHandshake('localhost', '/my/mock/path'); $client->connect(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } @@ -163,9 +177,8 @@ public static function uriStringAuthorizationDataProvider(): array #[DataProvider('uriStringAuthorizationDataProvider')] public function testUriStringAuthorization(string $uriAuth, string $expectedCredentials): void { - $this->expectStreamFactory(); - $client = new Client("wss://{$uriAuth}@localhost:8000/my/mock/path"); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("wss://{$uriAuth}@localhost:8000/my/mock/path", streamFactory: new StreamFactory()); $this->expectWsClientConnect(scheme: 'ssl'); $this->expectWsClientPerformHandshake( @@ -175,8 +188,11 @@ public function testUriStringAuthorization(string $uriAuth, string $expectedCred ); $client->connect(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } @@ -184,26 +200,28 @@ public function testUriInstanceImplementation(): void { $uri = new MockUri(); - $this->expectStreamFactory(); - $client = new Client($uri); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client($uri, streamFactory: new StreamFactory()); unset($client); } public function testTimeoutOption(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); + $client->setTimeout(300); $this->expectWsClientConnect(timeout: 300); $this->expectWsClientPerformHandshake('localhost:8000', '/my/mock/path'); $client->connect(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } @@ -215,9 +233,9 @@ public function testContext(): void $this->expectContextSetOption(); $context->setOptions(['ssl' => ['verify_peer' => false]]); - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); + $client->onHandshake(function (Client $client, Connection $connection) { $this->expectSocketStreamGetContext(); $connectionContext = $connection->getContext(); @@ -235,16 +253,19 @@ public function testContext(): void $this->assertSame($context, $client->getContext()); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testHeadersOption(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); + $client->addHeader('Generic-header', 'Generic content'); $this->expectWsClientConnect(); @@ -255,32 +276,37 @@ public function testHeadersOption(): void ); $client->connect(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testPersistentOption(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); + $client->setPersistent(true); $this->expectWsClientConnect(persistent: true); $this->expectWsClientPerformHandshake(); $client->connect(); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testConfigUnconnectedClient(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); $this->assertFalse($client->isConnected()); $client->setLogger(new NullLogger()); @@ -292,9 +318,8 @@ public function testConfigUnconnectedClient(): void public function testConfigConnectedClient(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); @@ -308,24 +333,35 @@ public function testConfigConnectedClient(): void }); $client->setTimeout(300); - $this->expectSocketStreamIsConnected(); $client->setFrameSize(64); $this->assertEquals(64, $client->getFrameSize()); + $this->expectStreamCollectionDetach(); + $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); + $client->disconnect(); + unset($client); } public function testHttpFactories(): void { - $httpFactory = new HttpFactory(); - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $httpFactory = new GuzzleFactory(); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); + + $this->assertSame($client, $client->setHttpFactory(HttpFactory::create($httpFactory))); - $this->assertSame($client, $client->setResponseFactory($httpFactory)); - $this->assertSame($client, $client->setRequestFactory($httpFactory)); - $this->assertSame($client, $client->setUriFactory($httpFactory)); + unset($client); + } + + public function testHStreamFactory(): void + { + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); + + $this->expectStreamFactory(); + $this->assertSame($client, $client->setStreamFactory(new StreamFactory())); unset($client); } diff --git a/tests/suites/client/HandshakeTest.php b/tests/suites/client/HandshakeTest.php index 769f09e..28d23e2 100644 --- a/tests/suites/client/HandshakeTest.php +++ b/tests/suites/client/HandshakeTest.php @@ -58,9 +58,8 @@ public function tearDown(): void public function testHandshakeResponse(): void { // Creating client - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); $this->assertFalse($client->isConnected()); $this->assertEquals(4096, $client->getFrameSize()); @@ -74,17 +73,14 @@ public function testHandshakeResponse(): void $this->assertEquals(101, $response->getStatusCode()); $this->assertEquals('Switching Protocols', $response->getReasonPhrase()); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($client); } public function testHandshakeResponseVariant(): void { // Creating client - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); $this->assertFalse($client->isConnected()); $this->assertEquals(4096, $client->getFrameSize()); @@ -119,16 +115,13 @@ function (string $method, array $params): void { $this->assertEquals(101, $response->getStatusCode()); $this->assertEquals('Switching Protocols', $response->getReasonPhrase()); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($client); } public function testHandshakeConnectionFailure(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectSocketStreamWrite(); @@ -137,8 +130,8 @@ public function testHandshakeConnectionFailure(): void }); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamGetMetadata(); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); +// $this->expectSocketStreamIsConnected(); +// $this->expectSocketStreamClose(); $this->expectException(ConnectionFailureException::class); $this->expectExceptionMessage('Connection error'); $client->connect(); @@ -148,9 +141,8 @@ public function testHandshakeConnectionFailure(): void public function testHandshakeUpgradeStatusFailure(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectSocketStreamWrite(); @@ -162,8 +154,8 @@ public function testHandshakeUpgradeStatusFailure(): void }); $this->expectException(HandshakeException::class); $this->expectExceptionMessage('Invalid status code 200.'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); +// $this->expectSocketStreamIsConnected(); +// $this->expectSocketStreamClose(); $client->connect(); unset($client); @@ -171,9 +163,8 @@ public function testHandshakeUpgradeStatusFailure(): void public function testHandshakeUpgradeHeadersFailure(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectSocketStreamWrite(); @@ -188,8 +179,8 @@ public function testHandshakeUpgradeHeadersFailure(): void }); $this->expectException(HandshakeException::class); $this->expectExceptionMessage('Connection to \'ws://localhost:8000/my/mock/path\' failed'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); +// $this->expectSocketStreamIsConnected(); +// $this->expectSocketStreamClose(); $client->connect(); unset($client); @@ -197,9 +188,8 @@ public function testHandshakeUpgradeHeadersFailure(): void public function testHandshakeUpgradeKeyFailure(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectSocketStreamWrite(); @@ -217,8 +207,8 @@ public function testHandshakeUpgradeKeyFailure(): void }); $this->expectException(HandshakeException::class); $this->expectExceptionMessage('Server sent bad upgrade response'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); +// $this->expectSocketStreamIsConnected(); +// $this->expectSocketStreamClose(); $client->connect(); unset($client); @@ -226,15 +216,15 @@ public function testHandshakeUpgradeKeyFailure(): void public function testHandshakeReconnect(): void { - $this->expectStreamFactory(); - $client = new Client('ws://localhost:8000/my/mock/path'); - $client->setStreamFactory(new StreamFactory()); + $this->expectWsClientCreate(); + $client = new Client("ws://localhost:8000/my/mock/path", streamFactory: new StreamFactory()); $this->expectWsClientConnect(); $this->expectSocketStreamWrite(); $this->expectSocketStreamReadLine()->setReturn(function () { throw new ReconnectException(new Uri('ws://localhost:8000/my/new/path')); }); + $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamClose(); @@ -247,8 +237,9 @@ public function testHandshakeReconnect(): void $this->assertEquals(101, $response->getStatusCode()); $this->assertEquals('Switching Protocols', $response->getReasonPhrase()); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); +// $this->expectSocketStreamIsConnected(); +// $this->expectSocketStreamClose(); + unset($client); } } diff --git a/tests/suites/connection/ConnectionTest.php b/tests/suites/connection/ConnectionTest.php index 628943a..34e933e 100644 --- a/tests/suites/connection/ConnectionTest.php +++ b/tests/suites/connection/ConnectionTest.php @@ -117,28 +117,6 @@ public function testCreate(): void unset($connection); } - public function testDestruct(): void - { - $temp = tmpfile(); - - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $stream = new SocketStream($temp); - - $this->expectSocketStreamGetLocalName(); - $this->expectSocketStreamGetRemoteName(); - $connection = new Connection($stream, false, false); - - $this->expectSocketStreamIsConnected(); - $this->assertTrue($connection->isConnected()); - - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); - - unset($connection); - } - public function testHttpMessages(): void { $temp = tmpfile(); @@ -234,8 +212,6 @@ public function testSendHttpError(): void }); $this->expectException(ConnectionClosedException::class); $this->expectExceptionMessage('Connection has unexpectedly closed'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->pushHttp(new Request()); unset($connection); @@ -259,8 +235,6 @@ public function testPullHttpError(): void }); $this->expectException(ConnectionClosedException::class); $this->expectExceptionMessage('Connection has unexpectedly closed'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->pullHttp(); unset($connection); @@ -284,8 +258,6 @@ public function testSendMessageError(): void }); $this->expectException(ConnectionClosedException::class); $this->expectExceptionMessage('Connection has unexpectedly closed'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->send(new Text('Connection error')); unset($connection); @@ -309,8 +281,6 @@ public function testPullMessageError(): void }); $this->expectException(ConnectionClosedException::class); $this->expectExceptionMessage('Connection has unexpectedly closed'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->pullMessage(); unset($connection); diff --git a/tests/suites/connection/ExceptionTest.php b/tests/suites/connection/ExceptionTest.php index 48a77c9..572486e 100644 --- a/tests/suites/connection/ExceptionTest.php +++ b/tests/suites/connection/ExceptionTest.php @@ -64,8 +64,6 @@ public function testBadOpcodeException(): void $this->expectException(BadOpcodeException::class); $this->expectExceptionMessage('Bad Opcode'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->send(new Text('Bad Opcode')); unset($connection); @@ -88,8 +86,6 @@ public function testBadUriException(): void }); $this->expectException(BadUriException::class); $this->expectExceptionMessage('Bad URI'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->send(new Text('Bad URI')); unset($connection); @@ -112,8 +108,6 @@ public function testConnectionClosedException(): void }); $this->expectException(ConnectionClosedException::class); $this->expectExceptionMessage('Connection has unexpectedly closed'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->send(new Text('Connection has unexpectedly closed')); unset($connection); @@ -136,8 +130,6 @@ public function testConnectionFailureException(): void }); $this->expectException(ConnectionFailureException::class); $this->expectExceptionMessage('Connection error'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->send(new Text('Connection error')); unset($connection); @@ -160,8 +152,6 @@ public function testConnectionTimeoutException(): void }); $this->expectException(ConnectionTimeoutException::class); $this->expectExceptionMessage('Connection operation timeout'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->send(new Text('Connection operation timeout')); unset($connection); @@ -188,8 +178,6 @@ public function testGenericTimeoutException(): void }); $this->expectException(ConnectionTimeoutException::class); $this->expectExceptionMessage('Connection operation timeout'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->send(new Text('Timeout')); unset($connection); @@ -216,8 +204,6 @@ public function testGenericEofException(): void }); $this->expectException(ConnectionClosedException::class); $this->expectExceptionMessage('Connection has unexpectedly closed'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->send(new Text('Eof')); unset($connection); @@ -243,8 +229,6 @@ public function testGenericUnconnectedException(): void }); $this->expectException(ConnectionFailureException::class); $this->expectExceptionMessage('Connection error'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->send(new Text('Generic')); unset($connection); @@ -271,8 +255,6 @@ public function testGenericConnectedException(): void }); $this->expectException(ConnectionFailureException::class); $this->expectExceptionMessage('Connection error'); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->send(new Text('Generic')); unset($connection); @@ -292,8 +274,6 @@ public function testInvalidTimeout(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage("Invalid timeout '-1' provided"); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $connection->setTimeout(-1); unset($connection); @@ -313,8 +293,6 @@ public function testInvalidFrameSize(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage("Invalid frameSize '0' provided"); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); // @phpstan-ignore argument.type $connection->setFrameSize(0); diff --git a/tests/suites/middleware/CallbackTest.php b/tests/suites/middleware/CallbackTest.php index edb2c82..e1e9ef8 100644 --- a/tests/suites/middleware/CallbackTest.php +++ b/tests/suites/middleware/CallbackTest.php @@ -72,8 +72,6 @@ public function testIncoming(): void $message = $connection->pullMessage(); $this->assertEquals('Changed message', $message->getContent()); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($stream); } @@ -101,8 +99,6 @@ public function testOutgoing(): void $this->expectSocketStreamWrite(); $connection->send(new Text('Test message')); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($stream); } @@ -138,8 +134,6 @@ public function testHttpIncoming(): void $message = $connection->pullHttp(); $this->assertEquals('POST', $message->getMethod()); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($stream); } @@ -167,8 +161,6 @@ public function testHttpOutgoing(): void $message = $connection->pushHttp(new Response(200)); $this->assertEquals(400, $message->getStatusCode()); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($stream); } @@ -190,8 +182,6 @@ public function testTick(): void })); $connection->tick(); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($stream); } } diff --git a/tests/suites/middleware/DeflateCompressorTest.php b/tests/suites/middleware/DeflateCompressorTest.php index 4db48a0..731baf8 100644 --- a/tests/suites/middleware/DeflateCompressorTest.php +++ b/tests/suites/middleware/DeflateCompressorTest.php @@ -133,8 +133,6 @@ function (string $method, array $params): void { $this->assertNotNull($this->getConfiguration($connection)->inflator); $this->assertSame($inflator, $this->getConfiguration($connection)->inflator); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($connection); } @@ -228,8 +226,6 @@ function (string $method, array $params): void { $this->assertNotNull($connection->getMeta('compressionExtension.configuration')->inflator); $this->assertSame($deflator, $connection->getMeta('compressionExtension.configuration')->deflator); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($connection); } @@ -304,8 +300,6 @@ function (string $method, array $params): void { $this->assertNull($connection->getMeta('compressionExtension.compressor')); $this->assertNull($connection->getMeta('compressionExtension.configuration')); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($connection); } @@ -405,8 +399,6 @@ function (string $method, array $params): void { $this->assertNotNull($this->getConfiguration($connection)->inflator); $this->assertNotSame($inflator, $this->getConfiguration($connection)->inflator); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($connection); } @@ -505,8 +497,6 @@ function (string $method, array $params): void { $this->assertNotNull($connection->getMeta('compressionExtension.configuration')->inflator); $this->assertNotSame($deflator, $connection->getMeta('compressionExtension.configuration')->deflator); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($connection); } @@ -572,8 +562,6 @@ function (string $method, array $params): void { 'inflator' => null, ], $connection->getMeta('compressionExtension.configuration')); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($connection); } @@ -672,8 +660,6 @@ function (string $method, array $params): void { $this->assertNotNull($connection->getMeta('compressionExtension.configuration')->inflator); $this->assertNotSame($deflator, $connection->getMeta('compressionExtension.configuration')->deflator); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($connection); } diff --git a/tests/suites/middleware/FollowRedirectTest.php b/tests/suites/middleware/FollowRedirectTest.php index 1981120..d4079ce 100644 --- a/tests/suites/middleware/FollowRedirectTest.php +++ b/tests/suites/middleware/FollowRedirectTest.php @@ -71,8 +71,6 @@ public function testRedirect(): void $this->expectSocketStreamReadLine()->setReturn(function () { return "\r\n"; }); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $this->expectException(ReconnectException::class); $this->expectExceptionMessage('Reconnect requested: ws://redirect.to/new/target'); $connection->pullHttp(); @@ -103,8 +101,6 @@ public function testMaxRedirect(): void $this->expectSocketStreamReadLine()->setReturn(function () { return "\r\n"; }); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $this->expectException(HandshakeException::class); $this->expectExceptionMessage('Too many redirect attempts, giving up'); $connection->pullHttp(); @@ -133,8 +129,7 @@ public function testNoLocation(): void return "\r\n"; }); $response = $connection->pullHttp(); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); + unset($connection); } } diff --git a/tests/suites/middleware/PingIntervalTest.php b/tests/suites/middleware/PingIntervalTest.php index db564ce..fed595a 100644 --- a/tests/suites/middleware/PingIntervalTest.php +++ b/tests/suites/middleware/PingIntervalTest.php @@ -69,8 +69,6 @@ public function testPingInterval(): void }); $connection->tick(); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($stream); } } diff --git a/tests/suites/middleware/ProcessStackTest.php b/tests/suites/middleware/ProcessStackTest.php index f26381c..657b09c 100644 --- a/tests/suites/middleware/ProcessStackTest.php +++ b/tests/suites/middleware/ProcessStackTest.php @@ -84,8 +84,6 @@ public function testIncoming(): void $message = $connection->pullMessage(); $this->assertEquals('Test message<-C<-B<-A', $message->getContent()); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($stream); } @@ -134,8 +132,6 @@ public function testOutgoing(): void $this->expectSocketStreamWrite(); $connection->send(new Text('Test message')); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($stream); } } diff --git a/tests/suites/middleware/SubprotocolNegotiationTest.php b/tests/suites/middleware/SubprotocolNegotiationTest.php index 9a2b402..1ea1b3c 100644 --- a/tests/suites/middleware/SubprotocolNegotiationTest.php +++ b/tests/suites/middleware/SubprotocolNegotiationTest.php @@ -86,8 +86,6 @@ function (string $method, array $params): void { $this->assertEquals(['sp-2'], $response->getHeader('Sec-WebSocket-Protocol')); $this->assertEquals('sp-2', $connection->getMeta('subprotocolNegotiation.selected')); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($connection); } @@ -135,8 +133,6 @@ function (string $method, array $params): void { $this->assertEquals([], $response->getHeader('Sec-WebSocket-Protocol')); $this->assertNull($connection->getMeta('subprotocolNegotiation.selected')); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($connection); } @@ -181,8 +177,6 @@ function (string $method, array $params): void { return "\r\n"; }); $this->expectSocketStreamWrite(); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); $this->expectException(HandshakeException::class); $this->expectExceptionMessage('Could not resolve subprotocol.'); $connection->pullHttp(); @@ -241,8 +235,6 @@ function (string $method, array $params): void { $this->assertEquals(['sp-2'], $response->getHeader('Sec-WebSocket-Protocol')); $this->assertEquals('sp-2', $connection->getMeta('subprotocolNegotiation.selected')); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($connection); } @@ -300,8 +292,6 @@ function (string $method, array $params): void { $this->assertEquals([], $response->getHeader('Sec-WebSocket-Protocol')); $this->assertNull($connection->getMeta('subprotocolNegotiation.selected')); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($connection); } @@ -359,8 +349,6 @@ function (string $method, array $params): void { $this->assertEquals([], $response->getHeader('Sec-WebSocket-Protocol')); $this->assertNull($connection->getMeta('subprotocolNegotiation.selected')); - $this->expectSocketStreamIsConnected(); - $this->expectSocketStreamClose(); unset($connection); } } diff --git a/tests/suites/server/ConfigTest.php b/tests/suites/server/ConfigTest.php index 1349cad..645c1bc 100644 --- a/tests/suites/server/ConfigTest.php +++ b/tests/suites/server/ConfigTest.php @@ -9,8 +9,9 @@ namespace WebSocket\Test\Server; -use GuzzleHttp\Psr7\HttpFactory; +use GuzzleHttp\Psr7\HttpFactory as GuzzleFactory; use PHPUnit\Framework\TestCase; +use Phrity\Http\HttpFactory; use Phrity\Net\Mock\{ Context, StreamFactory, @@ -205,7 +206,7 @@ public function testContext(): void public function testHttpFactories(): void { - $httpFactory = new HttpFactory(); + $httpFactory = new GuzzleFactory(); $this->expectContext(); $context = new Context(); $this->expectContextSetOptions(); @@ -215,9 +216,7 @@ public function testHttpFactories(): void $this->expectWsServerCreate(); $server = new Server(8000, streamFactory: new StreamFactory()); - $this->assertSame($server, $server->setResponseFactory($httpFactory)); - $this->assertSame($server, $server->setServerRequestFactory($httpFactory)); - $this->assertSame($server, $server->setUriFactory($httpFactory)); + $this->assertSame($server, $server->setHttpFactory(HttpFactory::create($httpFactory))); $this->expectStreamCollectionDetach(); $server->disconnect(); From 2c37dc3451e0dfc372bc28652acfa39a3ce6076b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Sat, 20 Sep 2025 12:29:50 +0200 Subject: [PATCH 06/16] Configuration class --- composer.json | 3 +- docs/Migrate_3_4.md | 16 +++++- src/Client.php | 44 ++++++++-------- src/Configuration.php | 54 ++++++++++++++++++++ src/Connection.php | 48 ++++++----------- src/Message/MessageHandler.php | 28 +++------- src/Middleware/Callback.php | 8 ++- src/Middleware/CloseHandler.php | 17 +++--- src/Middleware/CompressionExtension.php | 12 ++--- src/Middleware/FollowRedirect.php | 13 +++-- src/Middleware/MiddlewareHandler.php | 47 ++++++----------- src/Middleware/MiddlewareInterface.php | 2 + src/Middleware/PingInterval.php | 11 ++-- src/Middleware/PingResponder.php | 9 ++-- src/Middleware/SubprotocolNegotiation.php | 18 +++---- src/Server.php | 4 +- src/Trait/ConfigurationTrait.php | 35 +++++++++++++ tests/suites/connection/ConnectionTest.php | 1 - tests/suites/middleware/ProcessStackTest.php | 2 - 19 files changed, 211 insertions(+), 161 deletions(-) create mode 100644 src/Configuration.php create mode 100644 src/Trait/ConfigurationTrait.php diff --git a/composer.json b/composer.json index 3a83665..84773d8 100644 --- a/composer.json +++ b/composer.json @@ -35,14 +35,13 @@ }, "require-dev": { "guzzlehttp/psr7": "^2.0", - "php-coveralls/php-coveralls": "^2.0", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.0 | ^11.0 | ^12.0", "phrity/logger-console": "^1.0", "phrity/net-mock": "dev-ws4 as 2.4.0", "phrity/util-errorhandler": "^1.1", "robiningelbrecht/phpunit-coverage-tools": "^1.9", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^4.0" }, "suggest": { "ext-zlib": "Required for per-message deflate compression", diff --git a/docs/Migrate_3_4.md b/docs/Migrate_3_4.md index c473920..80de7b5 100644 --- a/docs/Migrate_3_4.md +++ b/docs/Migrate_3_4.md @@ -14,7 +14,21 @@ Version 4.x has few changes compared to previous version. * Client `setContext()` - no longer accepts array input * Server `onConnect()` - use `onHandshake()` instead * Server `setContext()` - no longer accepts array input -* FrameHandler `setLogger()` - removed + +v4 uses the Configuration class to propagate configurations throughout internal classes. + +This affects the following classes; +* Connection +* FrameHandler +* MessageHandler +* MiddlewareHandler +* All middlewares + +He following methods (when applicable) are removed; +* `setLogger()` + +Instead these classes get the method; +* `setConfiguration(WebSocket\Configuration $configuration)` ## Extending diff --git a/src/Client.php b/src/Client.php index 6ce3d61..fb0e2d5 100644 --- a/src/Client.php +++ b/src/Client.php @@ -43,6 +43,7 @@ use WebSocket\Message\Message; use WebSocket\Middleware\MiddlewareInterface; use WebSocket\Trait\{ + ConfigurationTrait, ListenerTrait, LoggerAwareTrait, SendMethodsTrait, @@ -55,6 +56,7 @@ */ class Client implements LoggerAwareInterface, Stringable { + use ConfigurationTrait; /** @use ListenerTrait */ use ListenerTrait; use LoggerAwareTrait; @@ -91,7 +93,7 @@ class Client implements LoggerAwareInterface, Stringable /** * @param UriInterface|string $uri A ws/wss-URI */ - public function __construct(UriInterface|string $uri) + public function __construct(UriInterface|string $uri, Configuration|null $configuration = null) { $this->socketUri = $this->parseUri($uri); $this->initLogger(); @@ -102,6 +104,7 @@ public function __construct(UriInterface|string $uri) = $this->serverRequestFactory = $this->uriFactory = new DefaultHttpFactory(); + $this->initConfiguration($configuration); } /** @@ -133,10 +136,7 @@ public function setStreamFactory(StreamFactory $streamFactory): self */ public function setLogger(LoggerInterface $logger): void { - $this->logger = $logger; - if ($this->connection) { - $this->connection->setLogger($this->logger); - } + $this->getConfiguration()->setLogger($logger); } /** @@ -317,12 +317,12 @@ public function start(int|float|null $timeout = null): void { // Check if running if ($this->running) { - $this->logger->warning("[client] Client is already running"); + $this->configuration->getLogger()->warning("[client] Client is already running"); return; } $this->running = true; $reconnect = false; - $this->logger->info("[client] Client is running"); + $this->configuration->getLogger()->info("[client] Client is running"); $connection = $this->connection(); @@ -340,12 +340,12 @@ public function start(int|float|null $timeout = null): void $this->dispatch($message->getOpcode(), [$this, $connection, $message]); } catch (MessageLevelInterface $e) { // Error, but keep connection open - $this->logger->error("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[client] {$e->getMessage()}", ['exception' => $e]); $this->dispatch('error', [$this, $connection, $e]); } catch (ConnectionLevelInterface $e) { // Error, disconnect connection $this->disconnect(); - $this->logger->error("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[client] {$e->getMessage()}", ['exception' => $e]); $this->dispatch('error', [$this, $connection, $e]); } } @@ -357,7 +357,7 @@ public function start(int|float|null $timeout = null): void } catch (CloseException $e) { // Close connection $connection->close($e->getCloseStatus(), $e->getMessage()); - $this->logger->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); $this->dispatch('error', [$this, $connection, $e]); } catch (ReconnectException $e) { // Reconnect connection @@ -366,21 +366,21 @@ public function start(int|float|null $timeout = null): void $this->socketUri = $uri; } $connection->close(); - $this->logger->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); $this->dispatch('error', [$this, $connection, $e]); } catch (ExceptionInterface $e) { $this->disconnect(); $this->running = false; // Low-level error - $this->logger->error("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[client] {$e->getMessage()}", ['exception' => $e]); $this->dispatch('error', [$this, null, $e]); } catch (Throwable $e) { $this->disconnect(); $this->running = false; // Crash it - $this->logger->error("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[client] {$e->getMessage()}", ['exception' => $e]); throw $e; } gc_collect_cycles(); // Collect garbage @@ -398,7 +398,7 @@ public function start(int|float|null $timeout = null): void public function stop(): void { $this->running = false; - $this->logger->info("[client] Client is stopped"); + $this->configuration->getLogger()->info("[client] Client is stopped"); } /** @@ -468,7 +468,7 @@ public function connect(): void $stream = $client->connect(); } catch (Throwable $e) { $error = "Could not open socket to \"{$hostUri}\": {$e->getMessage()}"; - $this->logger->error("[client] {$error}", ['exception' => $e]); + $this->configuration->getLogger()->error("[client] {$error}", ['exception' => $e]); throw new ClientException($error); } $name = $stream->getRemoteName(); @@ -484,14 +484,14 @@ public function connect(): void ); $this->connection->setFrameSize($this->frameSize); $this->connection->setTimeout($this->timeout); - $this->connection->setLogger($this->logger); +// $this->connection->setLogger($this->configuration->getLogger()); foreach ($this->middlewares as $middleware) { $this->connection->addMiddleware($middleware); } if (!$this->isConnected()) { $error = "Invalid stream on \"{$hostUri}\"."; - $this->logger->error("[client] {$error}"); + $this->configuration->getLogger()->error("[client] {$error}"); throw new ClientException($error); } try { @@ -500,14 +500,14 @@ public function connect(): void $response = $this->performHandshake($this->socketUri, $this->connection); } } catch (ReconnectException $e) { - $this->logger->info("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->info("[client] {$e->getMessage()}", ['exception' => $e]); if ($uri = $e->getUri()) { $this->socketUri = $uri; } $this->connect(); return; } - $this->logger->info("[client] Client connected to {$this->socketUri}"); + $this->configuration->getLogger()->info("[client] Client connected to {$this->socketUri}"); $this->dispatch('handshake', [ $this, $this->connection, @@ -524,7 +524,7 @@ public function disconnect(): void { if ($this->connection && $this->isConnected()) { $this->connection->disconnect(); - $this->logger->info('[client] Client disconnected'); + $this->configuration->getLogger()->info('[client] Client disconnected'); $this->dispatch('disconnect', [$this, $this->connection]); } } @@ -616,11 +616,11 @@ protected function performHandshake(Uri $uri, Connection $connection): ResponseI throw new HandshakeException("Server sent bad upgrade response.", $response); } } catch (HandshakeException $e) { - $this->logger->error("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[client] {$e->getMessage()}", ['exception' => $e]); throw $e; } - $this->logger->debug("[client] Handshake on {$uri->getPath()}"); + $this->configuration->getLogger()->debug("[client] Handshake on {$uri->getPath()}"); $connection->setHandshakeRequest($request); $connection->setHandshakeResponse($response); diff --git a/src/Configuration.php b/src/Configuration.php new file mode 100644 index 0000000..01b4c44 --- /dev/null +++ b/src/Configuration.php @@ -0,0 +1,54 @@ +logger = $logger ?? new NullLogger(); + } + + public function __toString(): string + { + return $this->stringable(''); + } + + + /* ---------- Logger methods ----------------------------------------------------------------------------------- */ + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } +} diff --git a/src/Connection.php b/src/Connection.php index 19ab087..3edbbe7 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -20,10 +20,6 @@ ServerRequestFactoryInterface, UriFactoryInterface, }; -use Psr\Log\{ - LoggerAwareInterface, - LoggerInterface, -}; use Stringable; use Throwable; use WebSocket\Frame\FrameHandler; @@ -53,9 +49,8 @@ * WebSocket\Connection class. * A client/server connection, wrapping socket stream. */ -class Connection implements LoggerAwareInterface, Stringable +class Connection implements Stringable { - use LoggerAwareTrait; use SendMethodsTrait; use StringableTrait; @@ -75,6 +70,8 @@ class Connection implements LoggerAwareInterface, Stringable private array $meta = []; private bool $closed = false; + private Configuration $configuration; + /* ---------- Magic methods ------------------------------------------------------------------------------------ */ @@ -86,14 +83,15 @@ public function __construct( ResponseFactoryInterface|null $responseFactory = null, ServerRequestFactoryInterface|null $serverRequestFactory = null, UriFactoryInterface|null $uriFactory = null, + Configuration|null $configuration = null, ) { $this->stream = $stream; $this->httpHandler = new HttpHandler($this->stream, $ssl, $responseFactory, $serverRequestFactory, $uriFactory); - $this->messageHandler = new MessageHandler(new FrameHandler($this->stream, $pushMasked, $pullMaskedRequired)); - $this->middlewareHandler = new MiddlewareHandler($this->messageHandler, $this->httpHandler); + $this->messageHandler = new MessageHandler(new FrameHandler($this->stream, $pushMasked, $pullMaskedRequired), $configuration); + $this->middlewareHandler = new MiddlewareHandler($this->messageHandler, $this->httpHandler, $configuration); $this->localName = $this->stream->getLocalName() ?? ''; $this->remoteName = $this->stream->getRemoteName() ?? ''; - $this->initLogger(); + $this->configuration = $configuration ?? new Configuration(); } public function __destruct() @@ -111,18 +109,6 @@ public function __toString(): string /* ---------- Configuration ------------------------------------------------------------------------------------ */ - /** - * Set logger. - * @param LoggerInterface $logger Logger implementation - */ - public function setLogger(LoggerInterface $logger): void - { - $this->logger = $logger; - $this->messageHandler->setLogger($logger); - $this->middlewareHandler->setLogger($logger); - $this->logger->debug("[connection] Setting logger: " . get_class($logger)); - } - /** * Set time out on connection. * @param int<0, max>|float $timeout Timeout part in seconds @@ -136,7 +122,7 @@ public function setTimeout(int|float $timeout): self } $this->timeout = $timeout; $this->stream->setTimeout($timeout); - $this->logger->debug("[connection] Setting timeout: {$timeout} seconds"); + $this->configuration->getLogger()->debug("[connection] Setting timeout: {$timeout} seconds"); return $this; } @@ -190,7 +176,7 @@ public function getContext(): Context public function addMiddleware(MiddlewareInterface $middleware): self { $this->middlewareHandler->add($middleware); - $this->logger->debug("[connection] Added middleware: {$middleware}"); + $this->configuration->getLogger()->debug("[connection] Added middleware: {$middleware}"); return $this; } @@ -230,7 +216,7 @@ public function isWritable(): bool */ public function disconnect(): self { - $this->logger->info('[connection] Closing connection'); + $this->configuration->getLogger()->info('[connection] Closing connection'); $this->stream->close(); $this->closed = true; return $this; @@ -242,7 +228,7 @@ public function disconnect(): self */ public function closeRead(): self { - $this->logger->info('[connection] Closing further reading'); + $this->configuration->getLogger()->info('[connection] Closing further reading'); $this->stream->closeRead(); return $this; } @@ -253,7 +239,7 @@ public function closeRead(): self */ public function closeWrite(): self { - $this->logger->info('[connection] Closing further writing'); + $this->configuration->getLogger()->info('[connection] Closing further writing'); $this->stream->closeWrite(); return $this; } @@ -410,11 +396,11 @@ protected function throwException(Throwable $e): never { // Internal exceptions are handled and re-thrown if ($e instanceof ReconnectException) { - $this->logger->info("[connection] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->info("[connection] {$e->getMessage()}", ['exception' => $e]); throw $e; } if ($e instanceof ExceptionInterface) { - $this->logger->error("[connection] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[connection] {$e->getMessage()}", ['exception' => $e]); throw $e; } // External exceptions are converted to internal @@ -422,15 +408,15 @@ protected function throwException(Throwable $e): never $meta = $this->stream->getMetadata(); $json = json_encode($meta); if (!empty($meta['timed_out'])) { - $this->logger->error("[connection] {$e->getMessage()}", ['exception' => $e, 'meta' => $meta]); + $this->configuration->getLogger()->error("[connection] {$e->getMessage()}", ['exception' => $e, 'meta' => $meta]); throw new ConnectionTimeoutException(); } if (!empty($meta['eof'])) { - $this->logger->error("[connection] {$e->getMessage()}", ['exception' => $e, 'meta' => $meta]); + $this->configuration->getLogger()->error("[connection] {$e->getMessage()}", ['exception' => $e, 'meta' => $meta]); throw new ConnectionClosedException(); } } - $this->logger->error("[connection] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[connection] {$e->getMessage()}", ['exception' => $e]); throw new ConnectionFailureException(); } } diff --git a/src/Message/MessageHandler.php b/src/Message/MessageHandler.php index 87be81c..2b9a242 100644 --- a/src/Message/MessageHandler.php +++ b/src/Message/MessageHandler.php @@ -7,28 +7,21 @@ namespace WebSocket\Message; -use Psr\Log\{ - LoggerAwareInterface, - LoggerInterface, -}; use Stringable; +use WebSocket\Configuration; use WebSocket\Exception\BadOpcodeException; use WebSocket\Frame\{ Frame, FrameHandler, }; -use WebSocket\Trait\{ - LoggerAwareTrait, - StringableTrait, -}; +use WebSocket\Trait\StringableTrait; /** * WebSocket\Message\MessageHandler class. * Message/Frame handling. */ -class MessageHandler implements LoggerAwareInterface, Stringable +class MessageHandler implements Stringable { - use LoggerAwareTrait; use StringableTrait; private const DEFAULT_SIZE = 4096; @@ -36,17 +29,12 @@ class MessageHandler implements LoggerAwareInterface, Stringable private FrameHandler $frameHandler; /** @var array $frameBuffer */ private array $frameBuffer = []; + private Configuration $configuration; - public function __construct(FrameHandler $frameHandler) + public function __construct(FrameHandler $frameHandler, Configuration|null $configuration = null) { $this->frameHandler = $frameHandler; - $this->initLogger(); - } - - public function setLogger(LoggerInterface $logger): void - { - $this->logger = $logger; - $this->frameHandler->setLogger($logger); + $this->configuration = $configuration ?? new Configuration(); } /** @@ -62,7 +50,7 @@ public function push(Message $message, int $size = self::DEFAULT_SIZE): Message foreach ($frames as $frame) { $this->frameHandler->push($frame); } - $this->logger->info("[message-handler] Pushed {$message}", [ + $this->configuration->getLogger()->info("[message-handler] Pushed {$message}", [ 'opcode' => $message->getOpcode(), 'content-length' => $message->getLength(), 'frames' => count($frames), @@ -108,7 +96,7 @@ private function createMessage(array $frames): Message return $carry . $item->getPayload(); }, '')); $message->setCompress($frames[0]->getRsv1() ?? false); - $this->logger->info("[message-handler] Pulled {$message}", [ + $this->configuration->getLogger()->info("[message-handler] Pulled {$message}", [ 'opcode' => $message->getOpcode(), 'content-length' => $message->getLength(), 'frames' => count($frames), diff --git a/src/Middleware/Callback.php b/src/Middleware/Callback.php index 2162c26..a6c4f0d 100644 --- a/src/Middleware/Callback.php +++ b/src/Middleware/Callback.php @@ -9,12 +9,11 @@ use Closure; use Psr\Http\Message\MessageInterface; -use Psr\Log\LoggerAwareInterface; use Stringable; use WebSocket\Connection; use WebSocket\Message\Message; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -23,7 +22,6 @@ * Generic middleware using callbacks. */ class Callback implements - LoggerAwareInterface, ProcessHttpIncomingInterface, ProcessHttpOutgoingInterface, ProcessIncomingInterface, @@ -31,7 +29,7 @@ class Callback implements ProcessTickInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; private Closure|null $incoming; @@ -52,7 +50,7 @@ public function __construct( $this->httpIncoming = $httpIncoming; $this->httpOutgoing = $httpOutgoing; $this->tick = $tick; - $this->initLogger(); + $this->initConfiguration(); } public function processIncoming(ProcessStack $stack, Connection $connection): Message diff --git a/src/Middleware/CloseHandler.php b/src/Middleware/CloseHandler.php index c5eafb5..5d211ca 100644 --- a/src/Middleware/CloseHandler.php +++ b/src/Middleware/CloseHandler.php @@ -7,7 +7,6 @@ namespace WebSocket\Middleware; -use Psr\Log\LoggerAwareInterface; use Stringable; use WebSocket\Connection; use WebSocket\Message\{ @@ -15,7 +14,7 @@ Message }; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -23,14 +22,14 @@ * WebSocket\Middleware\CloseHandler class. * Handles close procedure. */ -class CloseHandler implements LoggerAwareInterface, ProcessIncomingInterface, ProcessOutgoingInterface, Stringable +class CloseHandler implements ProcessIncomingInterface, ProcessOutgoingInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; public function __construct() { - $this->initLogger(); + $this->initConfiguration(); } public function processIncoming(ProcessStack $stack, Connection $connection): Message @@ -41,13 +40,13 @@ public function processIncoming(ProcessStack $stack, Connection $connection): Me } if ($connection->isWritable()) { // Remote sent Close; acknowledge and close for further reading - $this->logger->debug("[close-handler] Received 'close', status: {$message->getCloseStatus()}"); + $this->configuration->getLogger()->debug("[close-handler] Received 'close', status: {$message->getCloseStatus()}"); $ack = "Close acknowledged: {$message->getCloseStatus()}"; $connection->closeRead(); $connection->send(new Close($message->getCloseStatus(), $ack)); } else { // Remote sent Close/Ack: disconnect - $this->logger->debug("[close-handler] Received 'close' acknowledge, disconnecting"); + $this->configuration->getLogger()->debug("[close-handler] Received 'close' acknowledge, disconnecting"); $connection->disconnect(); } return $message; @@ -61,11 +60,11 @@ public function processOutgoing(ProcessStack $stack, Connection $connection, Mes } if ($connection->isReadable()) { // Local sent Close: close for further writing, expect remote acknowledge - $this->logger->debug("[close-handler] Sent 'close', status: {$message->getCloseStatus()}"); + $this->configuration->getLogger()->debug("[close-handler] Sent 'close', status: {$message->getCloseStatus()}"); $connection->closeWrite(); } else { // Local sent Close/Ack: disconnect - $this->logger->debug("[close-handler] Sent 'close' acknowledge, disconnecting"); + $this->configuration->getLogger()->debug("[close-handler] Sent 'close' acknowledge, disconnecting"); $connection->disconnect(); } return $message; diff --git a/src/Middleware/CompressionExtension.php b/src/Middleware/CompressionExtension.php index a8f8027..68cba13 100644 --- a/src/Middleware/CompressionExtension.php +++ b/src/Middleware/CompressionExtension.php @@ -15,7 +15,6 @@ ResponseInterface, ServerRequestInterface, }; -use Psr\Log\LoggerAwareInterface; use Stringable; use WebSocket\Connection; use WebSocket\Message\{ @@ -25,7 +24,7 @@ }; use WebSocket\Middleware\CompressionExtension\CompressorInterface; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -35,14 +34,13 @@ * @see https://datatracker.ietf.org/doc/html/rfc7692 */ class CompressionExtension implements - LoggerAwareInterface, ProcessHttpOutgoingInterface, ProcessHttpIncomingInterface, ProcessIncomingInterface, ProcessOutgoingInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; /** @var array $compressors */ @@ -51,7 +49,7 @@ class CompressionExtension implements public function __construct(CompressorInterface ...$compressors) { $this->compressors = $compressors; - $this->initLogger(); + $this->initConfiguration(); } public function processHttpOutgoing( @@ -91,7 +89,7 @@ public function processHttpIncoming(ProcessHttpStack $stack, Connection $connect if ($preferred = $this->getPreferred($message)) { $connection->setMeta('compressionExtension.compressor', $preferred->compressor); $connection->setMeta('compressionExtension.configuration', $preferred->configuration); - $this->logger->debug( + $this->configuration->getLogger()->debug( "[permessage-compression] Using {$preferred->compressor}", (array)$preferred->configuration ); @@ -101,7 +99,7 @@ public function processHttpIncoming(ProcessHttpStack $stack, Connection $connect if ($preferred = $this->getPreferred($message)) { $connection->setMeta('compressionExtension.compressor', $preferred->compressor); $connection->setMeta('compressionExtension.configuration', $preferred->configuration); - $this->logger->debug( + $this->configuration->getLogger()->debug( "[permessage-compression] Using {$preferred->compressor}", (array)$preferred->configuration ); diff --git a/src/Middleware/FollowRedirect.php b/src/Middleware/FollowRedirect.php index addc826..de1e09d 100644 --- a/src/Middleware/FollowRedirect.php +++ b/src/Middleware/FollowRedirect.php @@ -14,7 +14,6 @@ MessageInterface, ResponseInterface, }; -use Psr\Log\LoggerAwareInterface; use Stringable; use WebSocket\Connection; use WebSocket\Exception\{ @@ -22,7 +21,7 @@ ReconnectException, }; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -30,9 +29,9 @@ * WebSocket\Middleware\CloseHandler class. * Handles close procedure. */ -class FollowRedirect implements LoggerAwareInterface, ProcessHttpIncomingInterface, Stringable +class FollowRedirect implements ProcessHttpIncomingInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; private int $limit; @@ -41,7 +40,7 @@ class FollowRedirect implements LoggerAwareInterface, ProcessHttpIncomingInterfa public function __construct(int $limit = 10) { $this->limit = $limit; - $this->initLogger(); + $this->initConfiguration(); } /** @@ -59,11 +58,11 @@ public function processHttpIncoming(ProcessHttpStack $stack, Connection $connect ) { $note = "{$this->attempts} of {$this->limit} redirect attempts"; if ($this->attempts > $this->limit) { - $this->logger->debug("[follow-redirect] Too many redirect attempts, giving up"); + $this->configuration->getLogger()->debug("[follow-redirect] Too many redirect attempts, giving up"); throw new HandshakeException("Too many redirect attempts, giving up", $message); } $this->attempts++; - $this->logger->debug("[follow-redirect] {$message->getStatusCode()} {$locationHeader} ($note)"); + $this->configuration->getLogger()->debug("[follow-redirect] {$message->getStatusCode()} {$locationHeader} ($note)"); throw new ReconnectException(new Uri($locationHeader)); } return $message; diff --git a/src/Middleware/MiddlewareHandler.php b/src/Middleware/MiddlewareHandler.php index 59ba495..f325703 100644 --- a/src/Middleware/MiddlewareHandler.php +++ b/src/Middleware/MiddlewareHandler.php @@ -9,12 +9,9 @@ use Closure; use Psr\Http\Message\MessageInterface; -use Psr\Log\{ - LoggerInterface, - LoggerAwareInterface, -}; use Stringable; use WebSocket\Connection; +use WebSocket\Configuration; use WebSocket\Http\HttpHandler; use WebSocket\Message\{ Message, @@ -29,9 +26,8 @@ * WebSocket\Middleware\MiddlewareHandler class. * Middleware handling. */ -class MiddlewareHandler implements LoggerAwareInterface, Stringable +class MiddlewareHandler implements Stringable { - use LoggerAwareTrait; use StringableTrait; // Processor collections @@ -51,29 +47,18 @@ class MiddlewareHandler implements LoggerAwareInterface, Stringable // Handlers private HttpHandler $httpHandler; private MessageHandler $messageHandler; + private Configuration $configuration; /** * Create MiddlewareHandler. * @param MessageHandler $messageHandler * @param HttpHandler $httpHandler */ - public function __construct(MessageHandler $messageHandler, HttpHandler $httpHandler) + public function __construct(MessageHandler $messageHandler, HttpHandler $httpHandler, Configuration|null $configuration = null) { $this->messageHandler = $messageHandler; $this->httpHandler = $httpHandler; - $this->initLogger(); - } - - /** - * Set logger on MiddlewareHandler and all LoggerAware middlewares. - * @param LoggerInterface $logger - */ - public function setLogger(LoggerInterface $logger): void - { - $this->logger = $logger; - foreach ($this->middlewares as $middleware) { - $this->attachLogger($middleware); - } + $this->configuration = $configuration ?? new Configuration(); } /** @@ -84,26 +69,26 @@ public function setLogger(LoggerInterface $logger): void public function add(MiddlewareInterface $middleware): self { if ($middleware instanceof ProcessIncomingInterface) { - $this->logger->info("[middleware-handler] Added incoming: {$middleware}"); + $this->configuration->getLogger()->info("[middleware-handler] Added incoming: {$middleware}"); $this->incoming[] = $middleware; } if ($middleware instanceof ProcessOutgoingInterface) { - $this->logger->info("[middleware-handler] Added outgoing: {$middleware}"); + $this->configuration->getLogger()->info("[middleware-handler] Added outgoing: {$middleware}"); $this->outgoing[] = $middleware; } if ($middleware instanceof ProcessHttpIncomingInterface) { - $this->logger->info("[middleware-handler] Added http incoming: {$middleware}"); + $this->configuration->getLogger()->info("[middleware-handler] Added http incoming: {$middleware}"); $this->httpIncoming[] = $middleware; } if ($middleware instanceof ProcessHttpOutgoingInterface) { - $this->logger->info("[middleware-handler] Added http outgoing: {$middleware}"); + $this->configuration->getLogger()->info("[middleware-handler] Added http outgoing: {$middleware}"); $this->httpOutgoing[] = $middleware; } if ($middleware instanceof ProcessTickInterface) { - $this->logger->info("[middleware-handler] Added tick: {$middleware}"); + $this->configuration->getLogger()->info("[middleware-handler] Added tick: {$middleware}"); $this->tick[] = $middleware; } - $this->attachLogger($middleware); + $middleware->setConfiguration($this->configuration); $this->middlewares[] = $middleware; return $this; } @@ -115,7 +100,7 @@ public function add(MiddlewareInterface $middleware): self */ public function processIncoming(Connection $connection): Message { - $this->logger->info("[middleware-handler] Processing incoming"); + $this->configuration->getLogger()->info("[middleware-handler] Processing incoming"); $stack = new ProcessStack($connection, $this->messageHandler, $this->incoming); return $stack->handleIncoming(); } @@ -129,7 +114,7 @@ public function processIncoming(Connection $connection): Message */ public function processOutgoing(Connection $connection, Message $message): Message { - $this->logger->info("[middleware-handler] Processing outgoing"); + $this->configuration->getLogger()->info("[middleware-handler] Processing outgoing"); $stack = new ProcessStack($connection, $this->messageHandler, $this->outgoing); return $stack->handleOutgoing($message); } @@ -141,7 +126,7 @@ public function processOutgoing(Connection $connection, Message $message): Messa */ public function processHttpIncoming(Connection $connection): MessageInterface { - $this->logger->info("[middleware-handler] Processing http incoming"); + $this->configuration->getLogger()->info("[middleware-handler] Processing http incoming"); $stack = new ProcessHttpStack($connection, $this->httpHandler, $this->httpIncoming); return $stack->handleHttpIncoming(); } @@ -154,7 +139,7 @@ public function processHttpIncoming(Connection $connection): MessageInterface */ public function processHttpOutgoing(Connection $connection, MessageInterface $message): MessageInterface { - $this->logger->info("[middleware-handler] Processing http outgoing"); + $this->configuration->getLogger()->info("[middleware-handler] Processing http outgoing"); $stack = new ProcessHttpStack($connection, $this->httpHandler, $this->httpOutgoing); return $stack->handleHttpOutgoing($message); } @@ -165,7 +150,7 @@ public function processHttpOutgoing(Connection $connection, MessageInterface $me */ public function processTick(Connection $connection): void { - $this->logger->info("[middleware-handler] Processing tick"); + $this->configuration->getLogger()->info("[middleware-handler] Processing tick"); $stack = new ProcessTickStack($connection, $this->tick); $stack->handleTick(); } diff --git a/src/Middleware/MiddlewareInterface.php b/src/Middleware/MiddlewareInterface.php index 4f5ce43..159f52f 100644 --- a/src/Middleware/MiddlewareInterface.php +++ b/src/Middleware/MiddlewareInterface.php @@ -7,6 +7,7 @@ namespace WebSocket\Middleware; +use WebSocket\Configuration; use Stringable; /** @@ -15,4 +16,5 @@ */ interface MiddlewareInterface extends Stringable { + public function setConfiguration(Configuration $configuration): self; } diff --git a/src/Middleware/PingInterval.php b/src/Middleware/PingInterval.php index 381c054..c4dcd6f 100644 --- a/src/Middleware/PingInterval.php +++ b/src/Middleware/PingInterval.php @@ -7,7 +7,6 @@ namespace WebSocket\Middleware; -use Psr\Log\LoggerAwareInterface; use Stringable; use WebSocket\Connection; use WebSocket\Message\{ @@ -15,7 +14,7 @@ Message }; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -23,9 +22,9 @@ * WebSocket\Middleware\PingInterval class. * Handles close procedure. */ -class PingInterval implements LoggerAwareInterface, ProcessOutgoingInterface, ProcessTickInterface, Stringable +class PingInterval implements ProcessOutgoingInterface, ProcessTickInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; private int|float|null $interval; @@ -33,7 +32,7 @@ class PingInterval implements LoggerAwareInterface, ProcessOutgoingInterface, Pr public function __construct(int|float|null $interval = null) { $this->interval = $interval; - $this->initLogger(); + $this->initConfiguration(); } public function processOutgoing(ProcessStack $stack, Connection $connection, Message $message): Message @@ -46,7 +45,7 @@ public function processTick(ProcessTickStack $stack, Connection $connection): vo { // Push if time exceeds timestamp for next ping if ($connection->isWritable() && microtime(true) >= $this->getNext($connection)) { - $this->logger->debug("[ping-interval] Auto-pushing ping"); + $this->configuration->getLogger()->debug("[ping-interval] Auto-pushing ping"); $connection->send(new Ping()); $this->setNext($connection); // Update timestamp for next ping } diff --git a/src/Middleware/PingResponder.php b/src/Middleware/PingResponder.php index 482465e..625fc11 100644 --- a/src/Middleware/PingResponder.php +++ b/src/Middleware/PingResponder.php @@ -7,7 +7,6 @@ namespace WebSocket\Middleware; -use Psr\Log\LoggerAwareInterface; use Stringable; use WebSocket\Connection; use WebSocket\Message\{ @@ -16,7 +15,7 @@ Message }; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -24,14 +23,14 @@ * WebSocket\Middleware\PingResponder class. * Responds on incoming ping messages. */ -class PingResponder implements LoggerAwareInterface, ProcessIncomingInterface, Stringable +class PingResponder implements ProcessIncomingInterface, Stringable { + use ConfigurationTrait; use StringableTrait; - use LoggerAwareTrait; public function __construct() { - $this->initLogger(); + $this->initConfiguration(); } public function processIncoming(ProcessStack $stack, Connection $connection): Message diff --git a/src/Middleware/SubprotocolNegotiation.php b/src/Middleware/SubprotocolNegotiation.php index dcaacfe..167f778 100644 --- a/src/Middleware/SubprotocolNegotiation.php +++ b/src/Middleware/SubprotocolNegotiation.php @@ -15,12 +15,11 @@ ResponseInterface, ServerRequestInterface, }; -use Psr\Log\LoggerAwareInterface; use Stringable; use WebSocket\Connection; use WebSocket\Exception\HandshakeException; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -29,12 +28,11 @@ * Handles close procedure. */ class SubprotocolNegotiation implements - LoggerAwareInterface, ProcessHttpOutgoingInterface, ProcessHttpIncomingInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; /** @var array $subprotocols */ @@ -46,7 +44,7 @@ public function __construct(array $subprotocols, bool $require = false) { $this->subprotocols = $subprotocols; $this->require = $require; - $this->initLogger(); + $this->initConfiguration(); } public function processHttpOutgoing( @@ -60,13 +58,13 @@ public function processHttpOutgoing( $message = $message->withAddedHeader('Sec-WebSocket-Protocol', $subprotocol); } if ($supported = implode(', ', $this->subprotocols)) { - $this->logger->debug("[subprotocol-negotiation] Requested subprotocols: {$supported}"); + $this->configuration->getLogger()->debug("[subprotocol-negotiation] Requested subprotocols: {$supported}"); } } elseif ($message instanceof ResponseInterface) { // Outgoing Response on Server if ($selected = $connection->getMeta('subprotocolNegotiation.selected')) { $message = $message->withHeader('Sec-WebSocket-Protocol', $selected); - $this->logger->info("[subprotocol-negotiation] Selected subprotocol: {$selected}"); + $this->configuration->getLogger()->info("[subprotocol-negotiation] Selected subprotocol: {$selected}"); } elseif ($this->require) { // No matching subprotocol, fail handshake $message = $message->withStatus(426); @@ -86,10 +84,10 @@ public function processHttpIncoming(ProcessHttpStack $stack, Connection $connect if ($message instanceof ServerRequestInterface) { // Incoming requests on Server if ($requested = $message->getHeaderLine('Sec-WebSocket-Protocol')) { - $this->logger->debug("[subprotocol-negotiation] Requested subprotocols: {$requested}"); + $this->configuration->getLogger()->debug("[subprotocol-negotiation] Requested subprotocols: {$requested}"); } if ($supported = implode(', ', $this->subprotocols)) { - $this->logger->debug("[subprotocol-negotiation] Supported subprotocols: {$supported}"); + $this->configuration->getLogger()->debug("[subprotocol-negotiation] Supported subprotocols: {$supported}"); } foreach ($message->getHeader('Sec-WebSocket-Protocol') as $subprotocol) { if (in_array($subprotocol, $this->subprotocols)) { @@ -101,7 +99,7 @@ public function processHttpIncoming(ProcessHttpStack $stack, Connection $connect // Incoming Response on Client if ($selected = $message->getHeaderLine('Sec-WebSocket-Protocol')) { $connection->setMeta('subprotocolNegotiation.selected', $selected); - $this->logger->info("[subprotocol-negotiation] Selected subprotocol: {$selected}"); + $this->configuration->getLogger()->info("[subprotocol-negotiation] Selected subprotocol: {$selected}"); } elseif ($this->require) { // No matching subprotocol, close and fail $connection->close(); diff --git a/src/Server.php b/src/Server.php index 9b97612..94417e4 100644 --- a/src/Server.php +++ b/src/Server.php @@ -150,7 +150,7 @@ public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; foreach ($this->connections as $connection) { - $connection->setLogger($this->logger); +// $connection->setLogger($this->logger); } } @@ -576,7 +576,7 @@ protected function acceptHandler(SocketServer $socket): void throw new ConnectionFailureException("Server failed to accept: {$e->getMessage()}"); } try { - $connection->setLogger($this->logger); +// $connection->setLogger($this->logger); $connection ->setFrameSize($this->frameSize) ->setTimeout($this->timeout) diff --git a/src/Trait/ConfigurationTrait.php b/src/Trait/ConfigurationTrait.php new file mode 100644 index 0000000..675a60d --- /dev/null +++ b/src/Trait/ConfigurationTrait.php @@ -0,0 +1,35 @@ +configuration = $configuration ?? new Configuration(); + return $this; + } + + public function getConfiguration(): Configuration + { + return $this->configuration; + } + + public function setConfiguration(Configuration $configuration): self + { + $this->configuration = $configuration; + return $this; + } +} diff --git a/tests/suites/connection/ConnectionTest.php b/tests/suites/connection/ConnectionTest.php index 628943a..8da01c7 100644 --- a/tests/suites/connection/ConnectionTest.php +++ b/tests/suites/connection/ConnectionTest.php @@ -86,7 +86,6 @@ public function testCreate(): void $this->assertSame($connection, $connection->setTimeout(10)); $this->assertEquals(10, $connection->getTimeout()); - $connection->setLogger(new NullLogger()); $this->assertSame($connection, $connection->setFrameSize(64)); $this->assertEquals(64, $connection->getFrameSize()); $this->assertSame($connection, $connection->addMiddleware(new Callback())); diff --git a/tests/suites/middleware/ProcessStackTest.php b/tests/suites/middleware/ProcessStackTest.php index f26381c..5065581 100644 --- a/tests/suites/middleware/ProcessStackTest.php +++ b/tests/suites/middleware/ProcessStackTest.php @@ -73,7 +73,6 @@ public function testIncoming(): void $this->assertEquals('Test message<-C', $message->getContent()); return $message; })); - $connection->setLogger(new NullLogger()); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('gQw='); @@ -129,7 +128,6 @@ public function testOutgoing(): void $this->assertEquals('Test message->A->B->C<-C', $message->getContent()); return $message; })); - $connection->setLogger(new NullLogger()); $this->expectSocketStreamWrite(); $connection->send(new Text('Test message')); From e016576b2403cce4262ecf00e31f6908ce181b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Mon, 29 Sep 2025 13:46:18 +0200 Subject: [PATCH 07/16] Docs --- docs/Client.md | 108 ++++++--------------- docs/Configuration.md | 217 ++++++++++++++++++++++++++++++++++++++++++ docs/Connection.md | 37 ++----- docs/Index.md | 1 + docs/Server.md | 63 ++---------- docu.json | 4 + 6 files changed, 267 insertions(+), 163 deletions(-) create mode 100644 docs/Configuration.md diff --git a/docs/Client.md b/docs/Client.md index e91b595..23ae6a7 100644 --- a/docs/Client.md +++ b/docs/Client.md @@ -4,9 +4,33 @@ The client can read and write on a WebSocket stream. + +## Subscribe operation + +If you want to subscribe to messages sent by server at any point, use the listener functions. + +```php +$client = new WebSocket\Client("wss://echo.websocket.org/"); +$client + // Add standard middlewares + ->addMiddleware(new WebSocket\Middleware\CloseHandler()) + ->addMiddleware(new WebSocket\Middleware\PingResponder()) + // Listen to incoming Text messages + ->onText(function (WebSocket\Client $client, WebSocket\Connection $connection, WebSocket\Message\Message $message) { + // Act on incoming message + echo "Got message: {$message->getContent()} \n"; + // Possibly respond to server + $client->text("I got your your message"); + }) + ->start(); +``` +Optionally, `start()` can take timeout argument as int or float. + + ## Basic operation Set up a WebSocket client for request/response strategy. +Manually pulling messages using `receive()` method is not recommended. ```php $client = new WebSocket\Client("wss://echo.websocket.org/"); @@ -27,26 +51,6 @@ echo "Got message: {$message->getContent()} \n"; $client->close(); ``` -## Subscribe operation - -If you want to subscribe to messages sent by server at any point, use the listener functions. - -```php -$client = new WebSocket\Client("wss://echo.websocket.org/"); -$client - // Add standard middlewares - ->addMiddleware(new WebSocket\Middleware\CloseHandler()) - ->addMiddleware(new WebSocket\Middleware\PingResponder()) - // Listen to incoming Text messages - ->onText(function (WebSocket\Client $client, WebSocket\Connection $connection, WebSocket\Message\Message $message) { - // Act on incoming message - echo "Got message: {$message->getContent()} \n"; - // Possibly respond to server - $client->text("I got your your message"); - }) - ->start(); -``` -Optionally, `start()` can take timeout argument as int or float. ## Middlewares @@ -137,66 +141,16 @@ $client->close(1000, "Closing now"); The Client takes one argument: [URI](http://tools.ietf.org/html/rfc3986) as a class implementing [UriInterface](https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface) or as string. The client support `ws` (`tcp`) and `wss` (`ssl`) schemas, depending on SSL configuration. -Other options are available runtime by calling configuration methods. - -### Logger - -Client support adding any [PSR-4 compatible](https://www.php-fig.org/psr/psr-3/) logger. - -```php -$client->setLogger(Psr\Log\LoggerInterface $logger); -``` - -### Timeout -Timeout for various operations can be specified in seconds. -This affects how long Client will wait for connection, read and write operations, and listener scope. -Default is `60` seconds. Minimum is `0` seconds. Accepts int or float value. -Avoid setting very low values as it will cause a read loop to use all -available processing power even when there's nothing to read. +Other options are available using the Configuration class. -```php -$client->setTimeout(300); // set timeout in seconds -$client->getTimeout(); // => current timeout in seconds -``` - -### Frame size - -Defines the maximum payload per frame size in bytes. -Default is `4096` bytes. Minimum is `1` byte. -Do not change unless you have a strong reason to do so. - -```php -$client->setFrameSize(1024); // set maximum payload frame size in bytes -$client->getFrameSize(); // => current maximum payload frame size in bytes -``` - -### Persistent connection +- Logger +- Context +- Timeout +- Frame size +- Persistency -If set to true, the underlying connection will be kept open if possible. -This means that if Client closes and is then restarted, it may use the same connection. -Do not change unless you have a strong reason to do so. - -```php -$client->setPersistent(true); -``` - -### Context - -Client support adding [context options and parameters](https://www.php.net/manual/en/context.php) -using the [Phrity\Net\Context](https://github.com/sirn-se/phrity-net-stream?tab=readme-ov-file#context-class) class. - -```php -$context = new Phrity\Net\Context(); -$context->setOptions([ - "ssl" => [ - "verify_peer" => false, - "verify_peer_name" => false, - ], -]); -$client->setContext($context); // set context -$client->getContext(); // => currently used Phrity\Net\Context -``` +Read more on [Configuration](Configuration.md). ### HTTP factories diff --git a/docs/Configuration.md b/docs/Configuration.md new file mode 100644 index 0000000..877e391 --- /dev/null +++ b/docs/Configuration.md @@ -0,0 +1,217 @@ +[Documentation](Index.md) / Configuration + +# Websocket: Configuration + +The Configuration class is used to configure Client, Server and various worker classes. +It is delegated throughout class hierarchy unless explicitly set. + +## Using Configuration instance + +When creating a Configuration, all constructor arguments are optional. +```php +$configuration = new WebSocket\Configuration( + Psr\Log\LoggerInterface $logger, + Phrity\Net\Context $context, + int|float $timeout, + int $frameSize, + bool $persistent, + int $maxConnections, +); +``` + +Provide Configuration in constructor; +```php +$client = new WebSocket\Client( + uri: $uri, + configuration: $configuration, +); +$server = new WebSocket\Server( + port: $port, + ssl: $ssl, + configuration: $configuration, +); +``` + +Get and set Configuration; +```php +$configuration = $client->getConfiguration(); +$client->setConfiguration($configuration); +$configuration = $server->getConfiguration(); +$server->setConfiguration($configuration); +``` + + +## Configuration options + +### Logger + +``` +type: Psr\Log\LoggerInterface +default: Psr\Log\NullLogger +``` + +Attach any [PSR-4 compatible](https://www.php-fig.org/psr/psr-3/) logger. + +```php +// Configuration instance +$configuration = new WebSocket\Configuration(logger: $logger); +$configuration->setLogger($logger); +$logger = $configuration->getLogger(); + +// Convenience setters +$client->setLogger($logger); +$server->setLogger($logger); +``` + +### Context + +``` +type: Phrity\Net\Context +default: Phrity\Net\Context // Empty context +``` + +Client and server support adding [context options and parameters](https://www.php.net/manual/en/context.php) +using the [Phrity\Net\Context](https://github.com/sirn-se/phrity-net-stream?tab=readme-ov-file#context-class) class. + +```php +// Create and configure Context +$context = new Phrity\Net\Context(); +$context->setOptions([ + "ssl" => [ + "verify_peer" => false, + "verify_peer_name" => false, + ], +]); + +// Configuration instance +$configuration = new WebSocket\Configuration(context: $context); +$configuration->setContext($context); +$context = $configuration->getContext(); + +// Convenience getters/setters +$context = $client->getContext(); +$client->setContext($context); +$context = $server->getContext(); +$server->setContext($context); +``` + +### Timeout + +``` +type: int<0, max>|float<0, max> +default: 60 +``` + +Timeout for various operations can be specified in seconds. +This affects how long Client and Server will wait for connection, read and write operations, and listener scope. +Default is `60` seconds. Minimum is `0` seconds. Accepts int or float value. +Avoid setting very low values as it will cause a read loop to use all +available processing power even when there's nothing to read. + +```php +// Configuration instance +$configuration = new WebSocket\Configuration(timeout: $timeout); +$configuration->setTimeout($timeout); +$timeout = $configuration->getTimeout(); + +// Convenience getters/setters +$timeout = $client->getTimeout(); +$client->setTimeout($timeout); +$timeout = $server->getTimeout(); +$server->setTimeout($timeout); +``` + +### Frame size + +``` +type: int<1, max> +default: 4096 +``` + +Defines the maximum payload per frame size in bytes. +Default is `4096` bytes. Minimum is `1` byte. +Do not change unless you have a strong reason to do so. + +```php +// Configuration instance +$configuration = new WebSocket\Configuration(frameSize: $frameSize); +$configuration->setFrameSize($frameSize); +$frameSize = $configuration->getFrameSize(); + +// Convenience getters/setters +$frameSize = $client->getFrameSize(); +$client->setFrameSize($frameSize); +$frameSize = $server->getFrameSize(); +$server->setFrameSize($frameSize); +``` + +### Persistent connection (Client only) + +``` +type: bool +default: false +``` + +If set to true, the underlying connection will be kept open if possible. +This means that if Client closes and is then restarted, it may use the same connection. +Do not change unless you have a strong reason to do so. + +```php +// Configuration instance +$configuration = new WebSocket\Configuration(persistent: $persistent); +$configuration->setPersistent($persistent); +$persistent = $configuration->isPersistent(); + +// Convenience setter +$client->setPersistent($persistent); +``` + +### Max connections (Server only) + +``` +type: int<1, max>|null +default: null // Unlimited +``` + +Limit maximum number of connections served. Any additional connection attempts will fail. +By default Server support unlimited number of connections. + +```php +// Configuration instance +$configuration = new WebSocket\Configuration(maxConnections: $maxConnections); +$configuration->setMaxConnections($maxConnections); +$maxConnections = $configuration->getMaxConnections(); + +// Convenience setter +$server->setMaxConnections($maxConnections); +``` + +## Other configurable classes + +```php +// Connection class +$connection = new WebSocket\Connection(..., configuration: $configuration); +$configuration = $connection->getConfiguration(); +$connection->setConfiguration($configuration); + +// FrameHandler class +$frameHandler = new WebSocket\Frame\FrameHandler(..., configuration: $configuration); +$configuration = $frameHandler->getConfiguration(); +$frameHandler->setConfiguration($configuration); + +// MessageHandler class +$messageHandler = new WebSocket\Message\MessageHandler(..., configuration: $configuration); +$configuration = $messageHandler->getConfiguration(); +$messageHandler->setConfiguration($configuration); +``` + +If you need to set configuration on internal classes, best way is to clone the original; + +```php +$clonedConfiguration = clone $source->getConfiguration(); +$clonedConfiguration->setLogger(...); +$clonedConfiguration->setTimeout(...); +$clonedConfiguration->setFrameSize(...); +$source->setConfiguration($clonedConfiguration); +``` + diff --git a/docs/Connection.md b/docs/Connection.md index 0da4a8e..c56a41c 100644 --- a/docs/Connection.md +++ b/docs/Connection.md @@ -80,41 +80,18 @@ $connection->getHandshakeResponse(); ## Configuration -Some options are available runtime by calling configuration methods. - -### Logger - -Connection support adding any [PSR-4 compatible](https://www.php-fig.org/psr/psr-3/) logger. - +Configuration is normally inherited from Client or Sever, but can also be explicitly set. ```php -$connection->setLogger(Psr\Log\LoggerInterface $logger); +$configuration = $connection->getConfiguration(); +$connection->setConfiguration($configuration); ``` -### Timeout +- Logger +- Timeout -Timeout for various operations can be specified in seconds. -This affects how long a Connection will wait for read and write operations. -Default is `60` seconds. Minimum is `0` seconds. Accepts int or float value. -Avoid setting very low values as it will cause a read loop to use all -available processing power even when there's nothing to read. - -```php -$connection->setTimeout(300); // set timeout in seconds -$connection->getTimeout(); // => current timeout in seconds -``` - -### Frame size - -Defines the maximum payload per frame size in bytes. -Default is `4096` bytes. Minimum is `1` byte. -Do not change unless you have a strong reason to do so. - -```php -$connection->setFrameSize(1024); // set maximum payload frame size in bytes -$connection->getFrameSize(); // => current maximum payload frame size in bytes -``` +Read more on [Configuration](Configuration.md). -### Context +## Context Connection exposes [context options and parameters](https://www.php.net/manual/en/context.php) using the [Phrity\Net\Context](https://github.com/sirn-se/phrity-net-stream?tab=readme-ov-file#context-class) class. diff --git a/docs/Index.md b/docs/Index.md index f82a140..2b02c38 100644 --- a/docs/Index.md +++ b/docs/Index.md @@ -7,6 +7,7 @@ ## Resources +* [Configuration](Configuration.md) - Configuration for Client and Server * [Connection](Connection.md) - The connection between client and server * [Listener](Listener.md) - Listeners allow callbacks when messages are received * [Message](Message.md) - Represents a WebSocket message being sent or received diff --git a/docs/Server.md b/docs/Server.md index f044918..47396d8 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -138,56 +138,16 @@ $server->close(1000, "Closing now"); The Server takes two arguments; port and ssl. By default, ssl is false. If port is not specified, it will use 80 for non-secure and 443 for secure server. -Other options are available runtime by calling configuration methods. -### Logger +Other options are available using the Configuration class. -Server support adding any [PSR-4 compatible](https://www.php-fig.org/psr/psr-3/) logger. +- Logger +- Context +- Timeout +- Frame size +- Max connections -```php -$server->setLogger(Psr\Log\LoggerInterface $logger); -``` - -### Timeout - -Timeout for various operations can be specified in seconds. -This affects how long Server will wait for connection, read and write operations, and listener scope. -Default is `60` seconds. Minimum is `0` seconds. Accepts int or float value. -Avoid setting very low values as it will cause a read loop to use all -available processing power even when there's nothing to read. - -```php -$server->setTimeout(300); // set timeout in seconds -$server->getTimeout(); // => current timeout in seconds -``` - -### Frame size - -Defines the maximum payload per frame size in bytes. -Default is `4096` bytes. Minimum is `1` byte. -Do not change unless you have a strong reason to do so. - -```php -$server->setFrameSize(1024); // set maximum payload frame size in bytes -$server->getFrameSize(); // => current maximum payload frame size in bytes -``` - -### Context - -Server support adding [context options and parameters](https://www.php.net/manual/en/context.php) -using the [Phrity\Net\Context](https://github.com/sirn-se/phrity-net-stream?tab=readme-ov-file#context-class) class. - -```php -$context = new Phrity\Net\Context(); -$context->setOptions([ - "ssl" => [ - "verify_peer" => false, - "verify_peer_name" => false, - ], -]); -$server->setContext($context); // set context -$server->getContext(); // => currently used Phrity\Net\Context -``` +Read more on [Configuration](Configuration.md). ### HTTP factories @@ -210,15 +170,6 @@ $psrFactory = new Nyholm\Psr7\Factory\Psr17Factory(); // Or any other PSR-17 fac $server->setHttpFactory(Phrity\Http\HttpFactory::create($psrFactory)); ``` -### Max connections - -Limit maximum number of connections served. Any additional connection attempts will fail. -By default Server support unlimited number of connections. - -```php -$server->setMaxConnections(10); -``` - ### Server info Additional methids that provides information. diff --git a/docu.json b/docu.json index 29b1e36..5ca39ab 100644 --- a/docu.json +++ b/docu.json @@ -17,6 +17,10 @@ ], "name": "Resources", "content": [ + { + "name": "Configuration", + "source": "docs/Configuration.md" + }, { "name": "Connection", "source": "docs/Connection.md" From 6c2db33733b23c7757f269e24c271d25ea34ea83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Tue, 7 Oct 2025 13:45:56 +0200 Subject: [PATCH 08/16] Exceptions, docu --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- docs/Changelog.md | 1 + docs/Class_Synopsis.md | 817 ++++++++++++--------- docs/ErrorHandling.md | 53 ++ docs/Index.md | 1 + docu.json | 4 + src/Exception/MessageEncodingException.php | 16 + src/Message/Message.php | 6 +- tests/suites/message/CloseTest.php | 4 +- tests/suites/message/PingTest.php | 4 +- tests/suites/message/PongTest.php | 4 +- 11 files changed, 539 insertions(+), 373 deletions(-) create mode 100644 docs/ErrorHandling.md create mode 100644 src/Exception/MessageEncodingException.php diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5f0a00c..3876c4e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,7 +14,7 @@ Please provide a clear and concise description of the suspected issue. If possible, provide information - possibly including code snippets - on how to reproduce the issue. **Logs** -If possible, provide logs that indicate the issue. See https://github.com/Textalk/websocket-php/blob/master/docs/Examples.md#logger on how to use a logger. +If possible, provide logs that indicate the issue. See [logger example](../../docs/Examples.md#logger) on how to use a logger. **Versions** * Version of this library diff --git a/docs/Changelog.md b/docs/Changelog.md index 15e1333..54aef85 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -10,6 +10,7 @@ * Shared stream observer, sharable across multiple cients and servers (@sirn-se) * Configuration class for various settings (@sirn-se) + * MessageEncodingException when compression fails (@sirn-se) * Many class local configuration setters removed (@sirn-se) * Remove deprecated code (@sirn-se) diff --git a/docs/Class_Synopsis.md b/docs/Class_Synopsis.md index 05782da..a60891a 100644 --- a/docs/Class_Synopsis.md +++ b/docs/Class_Synopsis.md @@ -2,486 +2,577 @@ # Class Synopsis -Incomplete list of public API of core classes. +Public API of library classes. -## WebSocket +```php +abstract class WebSocket\Exception\Exception extends RuntimeException imlements WebSocket\Exception\ExceptionInterface +{ +} -### Client +abstract class WebSocket\Http\Message imlements Psr\Http\Message\MessageInterface, Stringable +{ + use WebSocket\Trait\StringableTrait; + + public method getAsArray(): array; + public method getBody(): Psr\Http\Message\StreamInterface; + public method getHeader(string $name): array; + public method getHeaderLine(string $name): string; + public method getHeaders(): array; + public method getProtocolVersion(): string; + public method hasHeader(string $name): bool; + public method withAddedHeader(string $name, mixed $value): self; + public method withBody(Psr\Http\Message\StreamInterface $body): self; + public method withHeader(string $name, mixed $value): self; + public method withProtocolVersion(string $version): self; + public method withoutHeader(string $name): self; +} -```php -class WebSocket\Client implements Psr\Log\LoggerAwareInterface, Stringable -{ - // Magic methods - public function __construct(Psr\Http\Message\UriInterface|string $uri); - public function __toString(): string; - - // Configuration - public function setStreamFactory(Phrity\Net\StreamFactory $streamFactory): self; - public function setLogger(Psr\Log\LoggerInterface $logger): void; - public function setTimeout(int|float $timeout): self; - public function getTimeout(): int|float; - public function setFrameSize(int $frameSize): self; - public function getFrameSize(): int; - public function setPersistent(bool $persistent): self; - public function getContext(): Phrity\Net\Context; - public function setContext(Phrity\Net\Context $context): self; - public function addHeader(string $name, string $content): self; - public function addMiddleware(WebSocket\Middleware\MiddlewareInterface $middleware): self; - - // Messaging operations - public function send(WebSocket\Message\Message $message): WebSocket\Message\Message; - public function receive(): WebSocket\Message\Message; - public function text(string $message): WebSocket\Message\Text; - public function binary(string $message): WebSocket\Message\Binary; - public function ping(string $message = ''): WebSocket\Message\Ping; - public function pong(string $message = ''): WebSocket\Message\Pong; - public function close(int $status = 1000, string $message = 'ttfn'): WebSocket\Message\Close; - - // Listener operations - public function start(int|float|null $timeout = null): void; - public function stop(): void; - public function isRunning(): bool; - - // Connection management - public function isConnected(): bool; - public function isReadable(): bool; - public function isWritable(): bool; - public function connect(): void; - public function disconnect(): void; - - // Connection wrapper methods - public function getName(): string|null; - public function getRemoteName(): string|null; - public function getHandshakeResponse(): WebSocket\Http\Response|null; - - // Listener methods - public function onDisconnect(Closure $closure): self; - public function onHandshake(Closure $closure): self; - public function onText(Closure $closure): self; - public function onBinary(Closure $closure): self; - public function onPing(Closure $closure): self; - public function onPong(Closure $closure): self; - public function onClose(Closure $closure): self; - public function onError(Closure $closure): self; - public function onTick(Closure $closure): self; -} -``` - -### Server +abstract class WebSocket\Message\Message imlements Stringable +{ + use WebSocket\Trait\StringableTrait; + + public method __construct(string $content = ""); + public method getContent(): string; + public method getFrames(int $frameSize = 4096): array; + public method getLength(): int; + public method getOpcode(): string; + public method getPayload(): string; + public method getTimestamp(): DateTimeInterface; + public method hasContent(): bool; + public method isCompressed(): bool; + public method setCompress(bool $compress): void; + public method setContent(string $content = ""): void; + public method setPayload(string $payload = ""): void; +} -```php -class WebSocket\Server implements Psr\Log\LoggerAwareInterface, Stringable -{ - // Magic methods - public function __construct(int $port = 80, bool $ssl = false); - public function __toString(): string; - - // Configuration - public function setStreamFactory(Phrity\Net\StreamFactory $streamFactory): self; - public function setLogger(Psr\Log\LoggerInterface $logger): void; - public function setTimeout(int|float $timeout): self; - public function getTimeout(): int|float; - public function setFrameSize(int $frameSize): self; - public function getFrameSize(): int; - public function getPort(): int; - public function getScheme(): string; - public function isSsl(): bool; - public function getContext(): Phrity\Net\Context; - public function setContext(Phrity\Net\Context $context): self; - public function getConnectionCount(): int; - public function getConnections(): array; - public function getReadableConnections(): array; - public function getWritableConnections(): array; - public function addMiddleware(WebSocket\Middleware\MiddlewareInterface $middleware): self; - public function setMaxConnections(int|null $maxConnections): self; - - // Messaging operations - public function send(WebSocket\Message\Message $message): WebSocket\Message\Message; - public function text(string $message): WebSocket\Message\Text; - public function binary(string $message): WebSocket\Message\Binary; - public function ping(string $message = ''): WebSocket\Message\Ping; - public function pong(string $message = ''): WebSocket\Message\Pong; - public function close(int $status = 1000, string $message = 'ttfn'): WebSocket\Message\Close; - - // Listener operations - public function start(int|float|null $timeout = null): void; - public function stop(): void; - public function isRunning(): bool; - - // Connection management - public function shutdown(int $closeStatus = 1001): void; - public function disconnect(): void; - - // Listener methods - public function onDisconnect(Closure $closure): self; - public function onHandshake(Closure $closure): self; - public function onText(Closure $closure): self; - public function onBinary(Closure $closure): self; - public function onPing(Closure $closure): self; - public function onPong(Closure $closure): self; - public function onClose(Closure $closure): self; - public function onError(Closure $closure): self; - public function onTick(Closure $closure): self; -} -``` - -### Connection +class WebSocket\Client imlements Psr\Log\LoggerAwareInterface, Stringable +{ + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\ListenerTrait; + use WebSocket\Trait\SendMethodsTrait; + use WebSocket\Trait\StringableTrait; + + public method __construct(Psr\Http\Message\UriInterface|string $uri, Phrity\Net\StreamFactory|null $streamFactory = null, Phrity\Http\HttpFactory|null $httpFactory = null, WebSocket\Runtime\Watcher|null $watcher = null, WebSocket\Configuration|null $configuration = null); + public method __toString(): string; + public method addHeader(string $name, string $content): self; + public method addMiddleware(WebSocket\Middleware\MiddlewareInterface $middleware): self; + public method connect(): void; + public method disconnect(): void; + public method getContext(): Phrity\Net\Context; + public method getFrameSize(): int; + public method getHandshakeResponse(): Psr\Http\Message\ResponseInterface|null; + public method getName(): string|null; + public method getRemoteName(): string|null; + public method getTimeout(): int|float; + public method isConnected(): bool; + public method isReadable(): bool; + public method isRunning(): bool; + public method isWritable(): bool; + public method receive(): WebSocket\Message\Message; + public method send(WebSocket\Message\Message $message): WebSocket\Message\Message; + public method setContext(Phrity\Net\Context $context): self; + public method setFrameSize(int $frameSize): self; + public method setHttpFactory(Phrity\Http\HttpFactory $httpFactory): self; + public method setLogger(Psr\Log\LoggerInterface $logger): void; + public method setPersistent(bool $persistent): self; + public method setStreamFactory(Phrity\Net\StreamFactory $streamFactory): self; + public method setTimeout(int|float $timeout): self; + public method start(int|float|null $timeout = null): void; + public method stop(): void; +} -```php -class WebSocket\Connection implements Psr\Log\LoggerAwareInterface, Stringable -{ - // Magic methods - public function __construct(Phrity\Net\SocketStream $stream, bool $pushMasked, bool $pullMaskedRequired, bool $ssl = false); - public function __destruct(); - public function __toString(): string; - - // Configuration - public function setLogger(Psr\Log\LoggerInterface $logger): void; - public function setTimeout(int|float $timeout): self; - public function getTimeout(): int|float; - public function setFrameSize(int $frameSize): self; - public function getFrameSize(): int; - public function getContext(): Phrity\Net\Context; - public function addMiddleware(WebSocket\Middleware\MiddlewareInterface $middleware): self; - - // Connection management - public function isConnected(): bool; - public function isReadable(): bool; - public function isWritable(): bool; - public function disconnect(): self; - public function closeRead(): self; - public function closeWrite(): self; - - // Connection state - public function getName(): string|null; - public function getRemoteName(): string|null; - public function setMeta(string $key, mixed $value): void; - public function getMeta(string $key): mixed; - public function tick(): void; - - // WebSocket Message methods - public function send(WebSocket\Message\Message $message): WebSocket\Message\Message; - public function pushMessage(WebSocket\Message\Message $message): WebSocket\Message\Message; - public function pullMessage(): WebSocket\Message\Message; - public function text(string $message): WebSocket\Message\Text; - public function binary(string $message): WebSocket\Message\Binary; - public function ping(string $message = ''): WebSocket\Message\Ping; - public function pong(string $message = ''): WebSocket\Message\Pong; - public function close(int $status = 1000, string $message = 'ttfn'): WebSocket\Message\Close; - - // HTTP Message methods - public function pushHttp(WebSocket\Http\Message $message): WebSocket\Http\Message; - public function pullHttp(): WebSocket\Http\Message; - public function setHandshakeRequest(WebSocket\Http\Request $request): self; - public function getHandshakeRequest(): WebSocket\Http\Request|null; - public function setHandshakeResponse(WebSocket\Http\Response $response): self; - public function getHandshakeResponse(): WebSocket\Http\Response|null; -} -``` - -## WebSocket / Exception - -### BadOpcodeException +class WebSocket\Configuration imlements Psr\Log\LoggerAwareInterface, Stringable +{ + use WebSocket\Trait\StringableTrait; + + public method __construct(Psr\Log\LoggerInterface|null $logger = null, Phrity\Net\Context|null $context = null, int|float|null $timeout = null, int|null $frameSize = null, bool|null $persistent = null, int|null $maxConnections = null); + public method __toString(): string; + public method getContext(): Phrity\Net\Context; + public method getFrameSize(): int; + public method getLogger(): Psr\Log\LoggerInterface; + public method getMaxConnections(): int|null; + public method getTimeout(): int|float; + public method isPersistent(): bool; + public method setContext(Phrity\Net\Context $context): void; + public method setFrameSize(int $frameSize): void; + public method setLogger(Psr\Log\LoggerInterface $logger): void; + public method setMaxConnections(int|null $maxConnections): void; + public method setPersistent(bool $persistent): void; + public method setTimeout(int|float $timeout): void; +} -```php -class WebSocket\Exception\BadOpcodeException extends WebSocket\Exception\Exception implements WebSocket\Exception\MessageLevelInterface +class WebSocket\Connection imlements Stringable { - public function __construct(string $message = 'Bad Opcode'); + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\SendMethodsTrait; + use WebSocket\Trait\StringableTrait; + + public method __construct(Phrity\Net\SocketStream $stream, bool $pushMasked, bool $pullMaskedRequired, bool $ssl = false, Phrity\Http\HttpFactory|null $httpFactory = null, WebSocket\Configuration|null $configuration = null); + public method __toString(): string; + public method addMiddleware(WebSocket\Middleware\MiddlewareInterface $middleware): self; + public method closeRead(): self; + public method closeWrite(): self; + public method disconnect(): self; + public method getContext(): Phrity\Net\Context; + public method getHandshakeRequest(): Psr\Http\Message\RequestInterface|null; + public method getHandshakeResponse(): Psr\Http\Message\ResponseInterface|null; + public method getMeta(string $key): mixed; + public method getName(): string; + public method getRemoteName(): string; + public method isConnected(): bool; + public method isReadable(): bool; + public method isWritable(): bool; + public method pullHttp(): Psr\Http\Message\MessageInterface; + public method pullMessage(): WebSocket\Message\Message; + public method pushHttp(Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; + public method pushMessage(WebSocket\Message\Message $message): WebSocket\Message\Message; + public method send(WebSocket\Message\Message $message): WebSocket\Message\Message; + public method setHandshakeRequest(Psr\Http\Message\RequestInterface $request): self; + public method setHandshakeResponse(Psr\Http\Message\ResponseInterface $response): self; + public method setMeta(string $key, mixed $value): void; + public method tick(): void; } -``` -### BadUriException +class WebSocket\Exception\BadOpcodeException extends WebSocket\Exception\Exception imlements WebSocket\Exception\MessageLevelInterface +{ + public method __construct(string $message = "Bad Opcode"); +} -```php class WebSocket\Exception\BadUriException extends WebSocket\Exception\Exception { - public function __construct(string $message = 'Bad URI'); + public method __construct(string $message = "Bad URI"); } -``` - -### ClientException -```php class WebSocket\Exception\ClientException extends WebSocket\Exception\Exception { } -``` - -### CloseException -```php class WebSocket\Exception\CloseException extends WebSocket\Exception\Exception { - public function __construct(int|null $status = null, string $content = ''); - public function getCloseStatus(): int|null; + public method __construct(int|null $status = null, string $content = ""); + public method getCloseStatus(): int; } -``` -### ConnectionClosedException +class WebSocket\Exception\ConnectionClosedException extends WebSocket\Exception\Exception imlements WebSocket\Exception\ConnectionLevelInterface +{ + public method __construct(); +} -```php -class WebSocket\Exception\ConnectionClosedException extends WebSocket\Exception\Exception implements WebSocket\Exception\ConnectionLevelInterface +class WebSocket\Exception\ConnectionFailureException extends WebSocket\Exception\Exception imlements WebSocket\Exception\ConnectionLevelInterface { - public function __construct(); + public method __construct(string|null $message = null); } -``` -### ConnectionFailureException +class WebSocket\Exception\ConnectionTimeoutException extends WebSocket\Exception\Exception imlements WebSocket\Exception\MessageLevelInterface +{ + public method __construct(); +} -```php -class WebSocket\Exception\ConnectionFailureException extends WebSocket\Exception\Exception implements WebSocket\Exception\ConnectionLevelInterface +class WebSocket\Exception\HandshakeException extends WebSocket\Exception\Exception imlements WebSocket\Exception\ConnectionLevelInterface { - public function __construct(); + public method __construct(string $message, Psr\Http\Message\ResponseInterface $response); + public method getResponse(): Psr\Http\Message\ResponseInterface; } -``` -### ConnectionLevelInterface +class WebSocket\Exception\ReconnectException extends WebSocket\Exception\Exception +{ + public method __construct(Phrity\Net\Uri|null $uri = null); + public method getUri(): Phrity\Net\Uri|null; +} -```php -interface WebSocket\Exception\ConnectionLevelInterface extends WebSocket\Exception\ExceptionInterface +class WebSocket\Exception\ServerException extends WebSocket\Exception\Exception { } -``` -### ConnectionTimeoutException +class WebSocket\Frame\Frame imlements Stringable +{ + use WebSocket\Trait\StringableTrait; + + public method __construct(string $opcode, string $payload, bool $final, bool $rsv1 = false, bool $rsv2 = false, bool $rsv3 = false); + public method __toString(): string; + public method getOpcode(): string; + public method getPayload(): string; + public method getPayloadLength(): int; + public method getRsv1(): bool; + public method getRsv2(): bool; + public method getRsv3(): bool; + public method isContinuation(): bool; + public method isFinal(): bool; + public method setRsv1(bool $rsv1): void; +} -```php -class WebSocket\Exception\ConnectionTimeoutException extends WebSocket\Exception\Exception implements WebSocket\Exception\MessageLevelInterface +class WebSocket\Frame\FrameHandler imlements Stringable { - public function __construct(); + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\OpcodeTrait; + use WebSocket\Trait\StringableTrait; + + public method __construct(Phrity\Net\SocketStream $stream, bool $pushMasked, bool $pullMaskedRequired, WebSocket\Configuration|null $configuration = null); + public method pull(): WebSocket\Frame\Frame; + public method push(WebSocket\Frame\Frame $frame): int; } -``` -### Exception +class WebSocket\Http\DefaultHttpFactory extends Phrity\Http\HttpFactory +{ + public method __construct(); + public method createRequest(string $method, mixed $uri): Psr\Http\Message\RequestInterface; + public method createResponse(int $code = 200, string $reasonPhrase = ""): Psr\Http\Message\ResponseInterface; + public method createServerRequest(string $method, mixed $uri, array $serverParams = []): Psr\Http\Message\ServerRequestInterface; + public method createUri(string $uri = ""): Psr\Http\Message\UriInterface; +} -```php -abstract class WebSocket\Exception\Exception extends RuntimeException implements WebSocket\Exception\ExceptionInterface +class WebSocket\Http\HttpHandler imlements Stringable { + use WebSocket\Trait\StringableTrait; + + public method __construct(Phrity\Net\SocketStream $stream, bool $ssl = false, Phrity\Http\HttpFactory|null $httpFactory = null); + public method pull(): Psr\Http\Message\MessageInterface; + public method push(Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; } -``` -### ExceptionInterface +class WebSocket\Http\Request extends WebSocket\Http\Message imlements Psr\Http\Message\RequestInterface +{ + public method __construct(string $method = "GET", Psr\Http\Message\UriInterface|string $uri = ""); + public method __toString(): string; + public method getAsArray(): array; + public method getMethod(): string; + public method getRequestTarget(): string; + public method getUri(): Psr\Http\Message\UriInterface; + public method withMethod(string $method): self; + public method withRequestTarget(mixed $requestTarget): self; + public method withUri(Psr\Http\Message\UriInterface $uri, bool $preserveHost = false): self; +} -```php -interface WebSocket\Exception\ExceptionInterface +class WebSocket\Http\Response extends WebSocket\Http\Message imlements Psr\Http\Message\ResponseInterface { + public method __construct(int $code = 200, string $reasonPhrase = ""); + public method __toString(): string; + public method getAsArray(): array; + public method getReasonPhrase(): string; + public method getStatusCode(): int; + public method withStatus(int $code, string $reasonPhrase = ""): self; } -``` -### HandshakeException +class WebSocket\Http\ServerRequest extends WebSocket\Http\Request imlements Psr\Http\Message\ServerRequestInterface +{ + public method __toString(): string; + public method getAttribute(string $name, mixed $default = null); + public method getAttributes(): array; + public method getCookieParams(): array; + public method getParsedBody(); + public method getQueryParams(): array; + public method getServerParams(): array; + public method getUploadedFiles(): array; + public method withAttribute(string $name, mixed $value): self; + public method withCookieParams(array $cookies): self; + public method withParsedBody(mixed $data): self; + public method withQueryParams(array $query): self; + public method withUploadedFiles(array $uploadedFiles): self; + public method withoutAttribute(string $name): self; +} -```php -class WebSocket\Exception\HandshakeException extends WebSocket\Exception\Exception implements WebSocket\Exception\ConnectionLevelInterface +class WebSocket\Message\Binary extends WebSocket\Message\Message { - public function __construct(string $message, WebSocket\Http\Response $response); - public function getResponse(): WebSocket\Http\Response; + public method isCompressed(): bool; + public method setCompress(bool $compress): void; } -``` -### MessageLevelInterface +class WebSocket\Message\Close extends WebSocket\Message\Message +{ + public method __construct(int|null $status = null, string $content = ""); + public method getCloseStatus(): int|null; + public method getPayload(): string; + public method setCloseStatus(int|null $status): void; + public method setPayload(string $payload = ""): void; +} -```php -interface WebSocket\Exception\MessageLevelInterface extends WebSocket\Exception\ExceptionInterface +class WebSocket\Message\MessageHandler imlements Stringable { + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\StringableTrait; + + public method __construct(WebSocket\Frame\FrameHandler $frameHandler, WebSocket\Configuration|null $configuration = null); + public method pull(): WebSocket\Message\Message; + public method push(WebSocket\Message\Message $message, int $size = WebSocket\Message\MessageHandler::DEFAULT_SIZE): WebSocket\Message\Message; } -``` -### ReconnectException +class WebSocket\Message\Ping extends WebSocket\Message\Message +{ +} -```php -class WebSocket\Exception\ReconnectException extends WebSocket\Exception\Exception +class WebSocket\Message\Pong extends WebSocket\Message\Message { - public function __construct(Uri|null $uri = null); } -``` -### ServerException +class WebSocket\Message\Text extends WebSocket\Message\Message +{ + public method isCompressed(): bool; + public method setCompress(bool $compress): void; +} -```php -class WebSocket\Exception\ServerException extends WebSocket\Exception\Exception +class WebSocket\Middleware\Callback imlements WebSocket\Middleware\ProcessHttpIncomingInterface, WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, WebSocket\Middleware\ProcessTickInterface, Stringable { + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\StringableTrait; + + public method __construct(Closure|null $incoming = null, Closure|null $outgoing = null, Closure|null $httpIncoming = null, Closure|null $httpOutgoing = null, Closure|null $tick = null); + public method processHttpIncoming(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection): Psr\Http\Message\MessageInterface; + public method processHttpOutgoing(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection, Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; + public method processIncoming(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection): WebSocket\Message\Message; + public method processOutgoing(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection, WebSocket\Message\Message $message): WebSocket\Message\Message; + public method processTick(WebSocket\Middleware\ProcessTickStack $stack, WebSocket\Connection $connection): void; } -``` -## WebSocket / Frame +class WebSocket\Middleware\CloseHandler imlements WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, Stringable +{ + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\StringableTrait; -### FrameHandler + public method __construct(); + public method processIncoming(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection): WebSocket\Message\Message; + public method processOutgoing(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection, WebSocket\Message\Message $message): WebSocket\Message\Message; +} -```php -class WebSocket\Frame\FrameHandler implements LoggerAwareInterface, Stringable +class WebSocket\Middleware\CompressionExtension imlements WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, Stringable { - public function __construct(Phrity\Net\SocketStream $stream, bool $pushMasked, bool $pullMaskedRequired); - public function __toString(): string; - public function pull(): WebSocket\Frame\Frame; - public function push(WebSocket\Frame\Frame $frame): int; + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\StringableTrait; + + public method __construct(WebSocket\Middleware\CompressionExtension\CompressorInterface $compressors); + public method processHttpIncoming(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection): Psr\Http\Message\MessageInterface; + public method processHttpOutgoing(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection, Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; + public method processIncoming(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection): WebSocket\Message\Message; + public method processOutgoing(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection, WebSocket\Message\Message $message): WebSocket\Message\Message; } -``` - -### Frame -```php -class WebSocket\Frame\Frame implements Stringable +class WebSocket\Middleware\CompressionExtension\DeflateCompressor imlements WebSocket\Middleware\CompressionExtension\CompressorInterface, Stringable { - public function __construct(string $opcode, string $payload, bool $final, bool $rsv1 = false, bool $rsv2 = false, bool $rsv3 = false); - public function __toString(): string; - public function isFinal(): bool; - public function getRsv1(): bool; - public function getRsv2(): bool; - public function getRsv3(): bool; - public function isContinuation(): bool; - public function getOpcode(): string; - public function getPayload(): string; - public function getPayloadLength(): int; + use WebSocket\Trait\StringableTrait; + + public method __construct(bool $serverNoContextTakeover = false, bool $clientNoContextTakeover = false, int $serverMaxWindowBits = WebSocket\Middleware\CompressionExtension\DeflateCompressor::MAX_WINDOW_SIZE, int $clientMaxWindowBits = WebSocket\Middleware\CompressionExtension\DeflateCompressor::MAX_WINDOW_SIZE, string $extension = "zlib"); + public method compress(WebSocket\Message\Binary|WebSocket\Message\Text $message, object $configuration): WebSocket\Message\Binary|WebSocket\Message\Text; + public method decompress(WebSocket\Message\Binary|WebSocket\Message\Text $message, object $configuration): WebSocket\Message\Binary|WebSocket\Message\Text; + public method getConfiguration(string $element, bool $isServer): object; + public method getRequestHeaderValue(): string; + public method getResponseHeaderValue(object $configuration): string; + public method isEligable(object $configuration): bool; } -``` -## WebSocket / Http +class WebSocket\Middleware\FollowRedirect imlements WebSocket\Middleware\ProcessHttpIncomingInterface, Stringable +{ + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\StringableTrait; -### HttpHandler + public method __construct(int $limit = 10); + public method processHttpIncoming(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection): Psr\Http\Message\MessageInterface; +} -```php -class WebSocket\Http\HttpHandler implements LoggerAwareInterface, Stringable +class WebSocket\Middleware\MiddlewareHandler imlements Stringable { - public function __construct(Phrity\Net\SocketStream $stream, bool $ssl = false); - public function __toString(): string; - public function pull(): Psr\Http\Message\MessageInterface; - public function push(Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\StringableTrait; + + public method __construct(WebSocket\Message\MessageHandler $messageHandler, WebSocket\Http\HttpHandler $httpHandler, WebSocket\Configuration|null $configuration = null); + public method add(WebSocket\Middleware\MiddlewareInterface $middleware): self; + public method processHttpIncoming(WebSocket\Connection $connection): Psr\Http\Message\MessageInterface; + public method processHttpOutgoing(WebSocket\Connection $connection, Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; + public method processIncoming(WebSocket\Connection $connection): WebSocket\Message\Message; + public method processOutgoing(WebSocket\Connection $connection, WebSocket\Message\Message $message): WebSocket\Message\Message; + public method processTick(WebSocket\Connection $connection): void; } -``` -### Message +class WebSocket\Middleware\PingInterval imlements WebSocket\Middleware\ProcessOutgoingInterface, WebSocket\Middleware\ProcessTickInterface, Stringable +{ + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\StringableTrait; -```php -abstract class WebSocket\Http\Message implements Psr\Http\Message\MessageInterface, Stringable + public method __construct(int|float|null $interval = null); + public method processOutgoing(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection, WebSocket\Message\Message $message): WebSocket\Message\Message; + public method processTick(WebSocket\Middleware\ProcessTickStack $stack, WebSocket\Connection $connection): void; +} + +class WebSocket\Middleware\PingResponder imlements WebSocket\Middleware\ProcessIncomingInterface, Stringable { - public function __toString(): string; - public function getProtocolVersion(): string; - public function withProtocolVersion(string $version): self; - public function getHeaders(): array; - public function hasHeader(string $name): bool; - public function getHeader(string $name): array; - public function getHeaderLine(string $name): string; - public function withHeader(string $name, mixed $value): self; - public function withAddedHeader(string $name, mixed $value): self; - public function withoutHeader(string $name): self; - public function getAsArray(): array; + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\StringableTrait; + + public method __construct(); + public method processIncoming(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection): WebSocket\Message\Message; } -``` -### Request +class WebSocket\Middleware\ProcessHttpStack imlements Stringable +{ + use WebSocket\Trait\StringableTrait; -```php -class WebSocket\Http\Request extends WebSocket\Http\Message implements Psr\Http\Message\RequestInterface + public method __construct(WebSocket\Connection $connection, WebSocket\Http\HttpHandler $httpHandler, array $processors); + public method handleHttpIncoming(): Psr\Http\Message\MessageInterface; + public method handleHttpOutgoing(Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; +} + +class WebSocket\Middleware\ProcessStack imlements Stringable { - public function __construct(string $method = 'GET', Psr\Http\Message\UriInterface|string|null $uri = null); - public function __toString(): string; - public function getRequestTarget(): string; - public function withRequestTarget(mixed $requestTarget): self; - public function getMethod(): string; - public function withMethod(string $method): self; - public function getUri(): Psr\Http\Message\UriInterface; - public function withUri(Psr\Http\Message\UriInterface $uri, bool $preserveHost = false): self; - public function getAsArray(): array; + use WebSocket\Trait\StringableTrait; + + public method __construct(WebSocket\Connection $connection, WebSocket\Message\MessageHandler $messageHandler, array $processors); + public method handleIncoming(): WebSocket\Message\Message; + public method handleOutgoing(WebSocket\Message\Message $message): WebSocket\Message\Message; } -``` -### Response +class WebSocket\Middleware\ProcessTickStack imlements Stringable +{ + use WebSocket\Trait\StringableTrait; -```php -class WebSocket\Http\Response extends WebSocket\Http\Message implements Psr\Http\Message\ResponseInterface + public method __construct(WebSocket\Connection $connection, array $processors); + public method handleTick(): void; +} + +class WebSocket\Middleware\SubprotocolNegotiation imlements WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, Stringable { - public function __construct(int $code = 200, string $reasonPhrase = ''); - public function __toString(): string; - public function getStatusCode(): int; - public function withStatus(int $code, string $reasonPhrase = ''): self; - public function getReasonPhrase(): string; - public function getAsArray(): array; + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\StringableTrait; + + public method __construct(array $subprotocols, bool $require = false); + public method processHttpIncoming(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection): Psr\Http\Message\MessageInterface; + public method processHttpOutgoing(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection, Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; } -``` -### ServerRequest +class WebSocket\Runtime\Watcher +{ + public method __construct(Phrity\Net\StreamCollection $streamCollection); + public method attach(string $key, Phrity\Net\StreamInterface $attach, Closure $onSelect): void; + public method detach(string $key): void; + public method isEmpty(): bool; + public method watch(float $timeout): void; +} -```php -class WebSocket\Http\ServerRequest extends WebSocket\Http\Request implements Psr\Http\Message\ServerRequestInterface +class WebSocket\Server imlements Psr\Log\LoggerAwareInterface, Stringable { - public function __toString(): string; + use WebSocket\Trait\ConfigurationTrait; + use WebSocket\Trait\ListenerTrait; + use WebSocket\Trait\SendMethodsTrait; + use WebSocket\Trait\StringableTrait; + + public method __construct(int $port = 80, bool $ssl = false, Phrity\Net\StreamFactory|null $streamFactory = null, Phrity\Http\HttpFactory|null $httpFactory = null, WebSocket\Runtime\Watcher|null $watcher = null, WebSocket\Configuration|null $configuration = null); + public method __toString(): string; + public method addMiddleware(WebSocket\Middleware\MiddlewareInterface $middleware): self; + public method disconnect(): void; + public method getConnectionCount(): int; + public method getConnections(): array; + public method getContext(): Phrity\Net\Context; + public method getFrameSize(): int; + public method getPort(): int; + public method getReadableConnections(): array; + public method getScheme(): string; + public method getTimeout(): int|float; + public method getWritableConnections(): array; + public method isRunning(): bool; + public method isSsl(): bool; + public method send(WebSocket\Message\Message $message): WebSocket\Message\Message; + public method setContext(Phrity\Net\Context $context): self; + public method setFrameSize(int $frameSize): self; + public method setHttpFactory(Phrity\Http\HttpFactory $httpFactory): self; + public method setLogger(Psr\Log\LoggerInterface $logger): void; + public method setMaxConnections(int|null $maxConnections): self; + public method setStreamFactory(Phrity\Net\StreamFactory $streamFactory): self; + public method setTimeout(int|float $timeout): self; + public method shutdown(int $closeStatus = 1001): void; + public method start(int|float|null $timeout = null): void; + public method stop(): void; } -``` -## WebSocket / Message +inteface WebSocket\Constant +{ + public const GUID; +} -### Binary +inteface WebSocket\Exception\ConnectionLevelInterface imlements WebSocket\Exception\ExceptionInterface +{ +} -```php -class WebSocket\Message\Binary extends WebSocket\Message\Message +inteface WebSocket\Exception\ExceptionInterface imlements Throwable { + public method getMessage(): string; } -``` -### Close +inteface WebSocket\Exception\MessageLevelInterface imlements WebSocket\Exception\ExceptionInterface +{ +} -```php -class WebSocket\Message\Close extends WebSocket\Message\Message +inteface WebSocket\Middleware\CompressionExtension\CompressorInterface imlements Stringable { - public function __construct(int|null $status = null, string $content = ''); - public function getCloseStatus(): int|null; - public function setCloseStatus(int|null $status): void; + public method compress(WebSocket\Message\Binary|WebSocket\Message\Text $message, object $configuration): WebSocket\Message\Binary|WebSocket\Message\Text; + public method decompress(WebSocket\Message\Binary|WebSocket\Message\Text $message, object $configuration): WebSocket\Message\Binary|WebSocket\Message\Text; + public method getConfiguration(string $element, bool $isServer): object; + public method getRequestHeaderValue(): string; + public method getResponseHeaderValue(object $configuration): string; + public method isEligable(object $configuration): bool; } -``` -### Message +inteface WebSocket\Middleware\MiddlewareInterface imlements Stringable +{ + public method setConfiguration(WebSocket\Configuration $configuration): self; +} -```php -class WebSocket\Message\Message implements Stringable +inteface WebSocket\Middleware\ProcessHttpIncomingInterface imlements WebSocket\Middleware\MiddlewareInterface { - public function __construct(string $content = ''); - public function __toString(): string; - public function getOpcode(): string; - public function getLength(): int; - public function getTimestamp(): DateTimeInterface; - public function getContent(): string; - public function setContent(string $content = ''): void; - public function hasContent(): bool; - public function getPayload(): string; - public function setPayload(string $payload = ''): void; - public function getFrames(int $frameSize = 4096): array; + public method processHttpIncoming(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection): Psr\Http\Message\MessageInterface; } -``` -### MessageHandler +inteface WebSocket\Middleware\ProcessHttpOutgoingInterface imlements WebSocket\Middleware\MiddlewareInterface +{ + public method processHttpOutgoing(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection, Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; +} -```php -class WebSocket\Message\MessageHandler implements Psr\Log\LoggerAwareInterface, Stringable +inteface WebSocket\Middleware\ProcessIncomingInterface imlements WebSocket\Middleware\MiddlewareInterface { - public function __construct(WebSocket\Frame\FrameHandler $frameHandler); - public function __toString(): string; - public function setLogger(Psr\Log\LoggerInterface $logger): void; - public function push(WebSocket\Message\Message $message, int $size = self::DEFAULT_SIZE): WebSocket\Message\Message; - public function pull(): WebSocket\Message\Message; + public method processIncoming(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection): WebSocket\Message\Message; } -``` -### Ping +inteface WebSocket\Middleware\ProcessOutgoingInterface imlements WebSocket\Middleware\MiddlewareInterface +{ + public method processOutgoing(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection, WebSocket\Message\Message $message): WebSocket\Message\Message; +} -```php -class WebSocket\Message\Ping extends WebSocket\Message\Message +inteface WebSocket\Middleware\ProcessTickInterface imlements WebSocket\Middleware\MiddlewareInterface { + public method processTick(WebSocket\Middleware\ProcessTickStack $stack, WebSocket\Connection $connection): void; } -``` -### Pong +trait WebSocket\Trait\ConfigurationTrait +{ + public method getConfiguration(): WebSocket\Configuration; + public method initConfiguration(WebSocket\Configuration|null $configuration = null): self; + public method setConfiguration(WebSocket\Configuration $configuration): self; +} -```php -class WebSocket\Message\Pong extends WebSocket\Message\Message +trait WebSocket\Trait\ListenerTrait { + public method onBinary(Closure $closure): self; + public method onClose(Closure $closure): self; + public method onDisconnect(Closure $closure): self; + public method onError(Closure $closure): self; + public method onHandshake(Closure $closure): self; + public method onPing(Closure $closure): self; + public method onPong(Closure $closure): self; + public method onText(Closure $closure): self; + public method onTick(Closure $closure): self; } -``` -### Text +trait WebSocket\Trait\OpcodeTrait +{ +} -```php -class WebSocket\Message\Text extends WebSocket\Message\Message +trait WebSocket\Trait\SendMethodsTrait { + public method binary(string $message): WebSocket\Message\Binary; + public method close(int $status = 1000, string $message = "ttfn"): WebSocket\Message\Close; + public method ping(string $message = ""): WebSocket\Message\Ping; + public method pong(string $message = ""): WebSocket\Message\Pong; + public method text(string $message): WebSocket\Message\Text; } -``` +trait WebSocket\Trait\StringableTrait +{ + public method __toString(): string; +} +``` \ No newline at end of file diff --git a/docs/ErrorHandling.md b/docs/ErrorHandling.md new file mode 100644 index 0000000..9404fc3 --- /dev/null +++ b/docs/ErrorHandling.md @@ -0,0 +1,53 @@ +[Documentation](Index.md) / Error handling + +# Websocket: Error handling + +## WebSocket errors + +Exceptions can occur on three levels; Message, Connection and root. +All internal errors has namespace `WebSocket\Exception` and implement `ExceptionInterface`. + +### Message errors + +Indicates that a message could not be sent or received. +The message affected is dropped as invalid but the connection will remain open for future send and receive actions. +All exceptions on this level implement MessageLevelInterface. + +- `BadOpcodeException` - When a message received has an invalid opcode +- `ConnectionTimeoutException` - Read or write operation on stream has timed out (see timeout configuration) +- `MessageEncodingException` - When message content could not be encoded or decoded, typically refers to compressed messages + +### Connection errors + +Indicates that the connection has failed, closed or must be closed for some reason. +The connection is no longer usable, but could possibly be reconnected. +All exceptions on this level implement ConnectionLevelInterface. + +- `ConnectionClosedException` - The underlying stream has closed +- `ConnectionFailureException` - The underlying stream has failed for unknown reason +- `HandshakeException` - The handshake procedure has failed + +### Root errors + +These errors indicate some critical error that can not be recovered. + +- `BadUriException` - The provided URI is invalid +- `ClientException` - The Client failed to connect to server +- `CloseException` - Connection is open but must be closed without any messages sent or received +- `ReconnectException` - Connection is open but must reconnect +- `ServerException` - The Server failed to open server socket + +## Additional exceptions + +These standard exceptions typically indicate a configuration error. + +- `BadMethodCallException` +- `InvalidArgumentException` +- `RangeException` +- `RuntimeException` + +## Handling errors + +When running the [listener](Listener.md), internal errors are caught, handled and forwarded to the `onError()` callback when possible. + +When using the `send()` and `receive()` methods, calling code need to deal with any exceptions thrown. diff --git a/docs/Index.md b/docs/Index.md index 2b02c38..dc37991 100644 --- a/docs/Index.md +++ b/docs/Index.md @@ -9,6 +9,7 @@ * [Configuration](Configuration.md) - Configuration for Client and Server * [Connection](Connection.md) - The connection between client and server +* [Error handling](ErrorHandling.md) - Notes on exceptions and error handling * [Listener](Listener.md) - Listeners allow callbacks when messages are received * [Message](Message.md) - Represents a WebSocket message being sent or received * [Middleware](Middleware.md) - Middlewares allow implementations to act incoming and outgoing messages diff --git a/docu.json b/docu.json index 5ca39ab..609dd44 100644 --- a/docu.json +++ b/docu.json @@ -25,6 +25,10 @@ "name": "Connection", "source": "docs/Connection.md" }, + { + "name": "Error handling", + "source": "docs/ErrorHandling.md" + }, { "name": "Listener", "source": "docs/Listener.md" diff --git a/src/Exception/MessageEncodingException.php b/src/Exception/MessageEncodingException.php new file mode 100644 index 0000000..4907fb3 --- /dev/null +++ b/src/Exception/MessageEncodingException.php @@ -0,0 +1,16 @@ +assertFalse($message->isCompressed()); - $this->expectException(ConnectionFailureException::class); + $this->expectException(MessageEncodingException::class); $this->expectExceptionMessage('Must not compress control message.'); $message->setCompress(true); } diff --git a/tests/suites/message/PingTest.php b/tests/suites/message/PingTest.php index a9edfb9..46e4f90 100644 --- a/tests/suites/message/PingTest.php +++ b/tests/suites/message/PingTest.php @@ -11,7 +11,7 @@ use PHPUnit\Framework\TestCase; use Stringable; -use WebSocket\Exception\ConnectionFailureException; +use WebSocket\Exception\MessageEncodingException; use WebSocket\Frame\Frame; use WebSocket\Message\{ Ping, @@ -63,7 +63,7 @@ public function testCompress(): void { $message = new Ping('Some content'); $this->assertFalse($message->isCompressed()); - $this->expectException(ConnectionFailureException::class); + $this->expectException(MessageEncodingException::class); $this->expectExceptionMessage('Must not compress control message.'); $message->setCompress(true); } diff --git a/tests/suites/message/PongTest.php b/tests/suites/message/PongTest.php index 636b971..1ed8f25 100644 --- a/tests/suites/message/PongTest.php +++ b/tests/suites/message/PongTest.php @@ -11,7 +11,7 @@ use PHPUnit\Framework\TestCase; use Stringable; -use WebSocket\Exception\ConnectionFailureException; +use WebSocket\Exception\MessageEncodingException; use WebSocket\Frame\Frame; use WebSocket\Message\{ Pong, @@ -63,7 +63,7 @@ public function testCompress(): void { $message = new Pong('Some content'); $this->assertFalse($message->isCompressed()); - $this->expectException(ConnectionFailureException::class); + $this->expectException(MessageEncodingException::class); $this->expectExceptionMessage('Must not compress control message.'); $message->setCompress(true); } From 6e607e0a2f78055f31317264d2ffdc8b25d05a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Fri, 17 Oct 2025 09:41:44 +0200 Subject: [PATCH 09/16] Docs --- src/Client.php | 94 ++++++++++++++++++++++++++++------ src/Connection.php | 77 +++++++++++++++++++++------- src/Message/MessageHandler.php | 12 +++-- 3 files changed, 145 insertions(+), 38 deletions(-) diff --git a/src/Client.php b/src/Client.php index 651a0e2..91d8c31 100644 --- a/src/Client.php +++ b/src/Client.php @@ -276,12 +276,16 @@ public function start(int|float|null $timeout = null): void { // Check if running if ($this->running) { - $this->configuration->getLogger()->warning("[client] Client is already running"); + $this->configuration->getLogger()->warning("[{scope}] Client is already running", [ + 'scope' => 'client', + ]); return; } $this->running = true; $reconnect = false; - $this->configuration->getLogger()->info("[client] Client is running"); + $this->configuration->getLogger()->info("[{scope}] Client is running", [ + 'scope' => 'client', + ]); $connection = $this->connection(); @@ -297,7 +301,11 @@ public function start(int|float|null $timeout = null): void } catch (CloseException $e) { // Close connection $connection->close($e->getCloseStatus(), $e->getMessage()); - $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'scope' => 'client', + ]); $this->dispatch('error', [$this, $connection, $e]); } catch (ReconnectException $e) { // Reconnect connection @@ -306,21 +314,33 @@ public function start(int|float|null $timeout = null): void $this->socketUri = $uri; } $connection->close(); - $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'scope' => 'client', + ]); $this->dispatch('error', [$this, $connection, $e]); } catch (ExceptionInterface $e) { $this->disconnect(); $this->running = false; // Low-level error - $this->configuration->getLogger()->error("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'scope' => 'client', + ]); $this->dispatch('error', [$this, null, $e]); } catch (Throwable $e) { $this->disconnect(); $this->running = false; // Crash it - $this->configuration->getLogger()->error("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'scope' => 'client', + ]); throw $e; } gc_collect_cycles(); // Collect garbage @@ -342,12 +362,20 @@ private function selectHandler(string $key, SocketStream $stream): void $this->dispatch($message->getOpcode(), [$this, $connection, $message]); } catch (MessageLevelInterface $e) { // Error, but keep connection open - $this->configuration->getLogger()->error("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'scope' => 'client', + ]); $this->dispatch('error', [$this, $connection, $e]); } catch (ConnectionLevelInterface $e) { // Error, disconnect connection $this->disconnect(); - $this->configuration->getLogger()->error("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'scope' => 'client', + ]); $this->dispatch('error', [$this, $connection, $e]); } } @@ -359,7 +387,9 @@ private function selectHandler(string $key, SocketStream $stream): void public function stop(): void { $this->running = false; - $this->configuration->getLogger()->info("[client] Client is stopped"); + $this->configuration->getLogger()->info("[{scope}] Client is stopped", [ + 'scope' => 'client', + ]); } /** @@ -428,7 +458,11 @@ public function connect(): void $stream = $client->connect(); } catch (Throwable $e) { $error = "Could not open socket to \"{$hostUri}\": {$e->getMessage()}"; - $this->configuration->getLogger()->error("[client] {$error}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'scope' => 'client', + ]); throw new ClientException($error); } $name = $stream->getRemoteName() ?? 'unknown'; @@ -449,7 +483,10 @@ public function connect(): void if (!$this->isConnected()) { $error = "Invalid stream on \"{$hostUri}\"."; - $this->configuration->getLogger()->error("[client] {$error}"); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'message' => $error, + 'scope' => 'client', + ]); $this->disconnect(); throw new ClientException($error); } @@ -459,14 +496,21 @@ public function connect(): void $response = $this->performHandshake($this->socketUri, $this->connection); } } catch (ReconnectException $e) { - $this->configuration->getLogger()->info("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->info("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'scope' => 'client', + ]); if ($uri = $e->getUri()) { $this->socketUri = $uri; } $this->connect(); return; } - $this->configuration->getLogger()->info("[client] Client connected to {$this->socketUri}"); + $this->configuration->getLogger()->info("[{scope}] Client connected to {$this->socketUri}", [ + 'scope' => 'client', + 'uri' => $this->socketUri, + ]); $this->dispatch('handshake', [ $this, $this->connection, @@ -486,7 +530,9 @@ public function disconnect(): void } if ($this->connection && $this->isConnected()) { $this->connection->disconnect(); - $this->configuration->getLogger()->info('[client] Client disconnected'); + $this->configuration->getLogger()->info("[{scope}] Client disconnected", [ + 'scope' => 'client', + ]); $this->dispatch('disconnect', [$this, $this->connection]); } } @@ -494,6 +540,15 @@ public function disconnect(): void /* ---------- Connection wrapper methods ----------------------------------------------------------------------- */ + public function getIdentity(): string + { + return sprintf( + 'client/%s/%s', + $this->getName() ?: 'closed', + $this->getRemoteName() ?: 'closed', + ); + } + /** * Get name of local socket, or null if not connected. * @return string|null @@ -578,11 +633,18 @@ protected function performHandshake(Uri $uri, Connection $connection): ResponseI throw new HandshakeException("Server sent bad upgrade response.", $response); } } catch (HandshakeException $e) { - $this->configuration->getLogger()->error("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'scope' => 'client', + ]); throw $e; } - $this->configuration->getLogger()->debug("[client] Handshake on {$uri->getPath()}"); + $this->configuration->getLogger()->debug("[{scope}] Handshake on {$uri->getPath()}", [ + 'path' => $uri->getPath(), + 'scope' => 'client', + ]); $connection->setHandshakeRequest($request); $connection->setHandshakeResponse($response); diff --git a/src/Connection.php b/src/Connection.php index 46ed11c..3d59883 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -76,12 +76,19 @@ public function __construct( Configuration|null $configuration = null, ) { $this->stream = $stream; + $this->initConfiguration($configuration); $this->httpHandler = new HttpHandler($this->stream, $ssl, $httpFactory); - $this->messageHandler = new MessageHandler(new FrameHandler($this->stream, $pushMasked, $pullMaskedRequired)); - $this->middlewareHandler = new MiddlewareHandler($this->messageHandler, $this->httpHandler); + $this->messageHandler = new MessageHandler( + new FrameHandler($this->stream, $pushMasked, $pullMaskedRequired, $this->configuration), + $this->configuration + ); + $this->middlewareHandler = new MiddlewareHandler( + $this->messageHandler, + $this->httpHandler, + $this->configuration + ); $this->localName = $this->stream->getLocalName() ?? ''; $this->remoteName = $this->stream->getRemoteName() ?? ''; - $this->initConfiguration($configuration); $this->stream->setTimeout($this->configuration->getTimeout()); } @@ -111,7 +118,10 @@ public function getContext(): Context public function addMiddleware(MiddlewareInterface $middleware): self { $this->middlewareHandler->add($middleware); - $this->configuration->getLogger()->debug("[connection] Added middleware: {$middleware}"); + $this->configuration->getLogger()->debug("[{scope}] Added middleware: {$middleware}", [ + 'middleware' => $middleware, + 'scope' => 'connection', + ]); return $this; } @@ -151,7 +161,9 @@ public function isWritable(): bool */ public function disconnect(): self { - $this->configuration->getLogger()->info('[connection] Closing connection'); + $this->configuration->getLogger()->info("[{scope}] Closing connection", [ + 'scope' => 'connection', + ]); $this->stream->close(); return $this; } @@ -162,7 +174,9 @@ public function disconnect(): self */ public function closeRead(): self { - $this->configuration->getLogger()->info('[connection] Closing further reading'); + $this->configuration->getLogger()->info("[{scope}] Closing further reading", [ + 'scope' => 'connection', + ]); $this->stream->closeRead(); return $this; } @@ -173,7 +187,9 @@ public function closeRead(): self */ public function closeWrite(): self { - $this->configuration->getLogger()->info('[connection] Closing further writing'); + $this->configuration->getLogger()->info("[{scope}] Closing further writing", [ + 'scope' => 'connection', + ]); $this->stream->closeWrite(); return $this; } @@ -181,6 +197,15 @@ public function closeWrite(): self /* ---------- Connection state --------------------------------------------------------------------------------- */ + public function getIdentity(): string + { + return sprintf( + 'connection/%s/%s', + $this->getName() ?: 'closed', + $this->getRemoteName() ?: 'closed', + ); + } + /** * Get name of local socket, or null if not connected. * @return string @@ -330,11 +355,19 @@ protected function throwException(Throwable $e): never { // Internal exceptions are handled and re-thrown if ($e instanceof ReconnectException) { - $this->configuration->getLogger()->info("[connection] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->info("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'scope' => 'connection', + ]); throw $e; } if ($e instanceof ExceptionInterface) { - $this->configuration->getLogger()->error("[connection] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'scope' => 'connection', + ]); throw $e; } // External exceptions are converted to internal @@ -342,21 +375,29 @@ protected function throwException(Throwable $e): never $meta = $this->stream->getMetadata(); $json = json_encode($meta); if (!empty($meta['timed_out'])) { - $this->configuration->getLogger()->error( - "[connection] {$e->getMessage()}", - ['exception' => $e, 'meta' => $meta] - ); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'meta' => $meta, + 'scope' => 'connection', + ]); throw new ConnectionTimeoutException(); } if (!empty($meta['eof'])) { - $this->configuration->getLogger()->error( - "[connection] {$e->getMessage()}", - ['exception' => $e, 'meta' => $meta] - ); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'meta' => $meta, + 'scope' => 'connection', + ]); throw new ConnectionClosedException(); } } - $this->configuration->getLogger()->error("[connection] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'scope' => 'connection', + ]); throw new ConnectionFailureException(); } } diff --git a/src/Message/MessageHandler.php b/src/Message/MessageHandler.php index a0764ac..67269dd 100644 --- a/src/Message/MessageHandler.php +++ b/src/Message/MessageHandler.php @@ -53,10 +53,12 @@ public function push(Message $message, int $size = self::DEFAULT_SIZE): Message foreach ($frames as $frame) { $this->frameHandler->push($frame); } - $this->configuration->getLogger()->info("[message-handler] Pushed {$message}", [ - 'opcode' => $message->getOpcode(), + $this->configuration->getLogger()->info("[{scope}] Pushed {message}", [ 'content-length' => $message->getLength(), 'frames' => count($frames), + 'message' => $message, + 'opcode' => $message->getOpcode(), + 'scope' => 'message-handler', ]); return $message; } @@ -99,10 +101,12 @@ private function createMessage(array $frames): Message return $carry . $item->getPayload(); }, '')); $message->setCompress($frames[0]->getRsv1() ?? false); - $this->configuration->getLogger()->info("[message-handler] Pulled {$message}", [ - 'opcode' => $message->getOpcode(), + $this->configuration->getLogger()->info("[{scope}] Pulled {message}", [ 'content-length' => $message->getLength(), 'frames' => count($frames), + 'message' => $message, + 'opcode' => $message->getOpcode(), + 'scope' => 'message-handler', ]); return $message; } From 02ffae314bcb0482a52d202c936ba73d66b36d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Thu, 23 Oct 2025 14:14:27 +0200 Subject: [PATCH 10/16] Fixup --- docs/Migrate_3_4.md | 14 +++--- src/Client.php | 9 ---- src/Cluster.php | 108 ++++++++++++++++++++++++++++++++++++++++++++ src/Connection.php | 9 ---- 4 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 src/Cluster.php diff --git a/docs/Migrate_3_4.md b/docs/Migrate_3_4.md index 2d6fc1e..6f1de0e 100644 --- a/docs/Migrate_3_4.md +++ b/docs/Migrate_3_4.md @@ -2,22 +2,22 @@ # Websocket: Migration v3 -> v4 -Version 4.x has few changes compared to previous version. +Version 4.x has few breaking changes compared to previous version. ## Breaking changes +* Client `setContext()` - no longer accepts array input +* Server `setContext()` - no longer accepts array input -## Removed deprecated code +## Removed deprecated methods * Client `getMeta()` - removed * Client `onConnect()` - use `onHandshake()` instead -* Client `setContext()` - no longer accepts array input * Server `onConnect()` - use `onHandshake()` instead -* Server `setContext()` - no longer accepts array input ## Configuration management -v4 uses the Configuration class to propagate configurations throughout internal classes. +`v4` uses the `Configuration` class to propagate configurations throughout internal classes. This affects the following classes; * Connection @@ -26,7 +26,7 @@ This affects the following classes; * MiddlewareHandler * All middlewares -He following methods (when applicable) are removed; +The following methods (when applicable) are removed; * `setLogger()` * `getTimeout()` * `setTimeout()` @@ -48,3 +48,5 @@ $source->setConfiguration($clonedConfiguration); ## Extending +Increased modularization affects many internal classes and methods. +If you rely on using these directly (extending or adapting) your code may be incompatible. diff --git a/src/Client.php b/src/Client.php index 91d8c31..6f11d0b 100644 --- a/src/Client.php +++ b/src/Client.php @@ -540,15 +540,6 @@ public function disconnect(): void /* ---------- Connection wrapper methods ----------------------------------------------------------------------- */ - public function getIdentity(): string - { - return sprintf( - 'client/%s/%s', - $this->getName() ?: 'closed', - $this->getRemoteName() ?: 'closed', - ); - } - /** * Get name of local socket, or null if not connected. * @return string|null diff --git a/src/Cluster.php b/src/Cluster.php new file mode 100644 index 0000000..45f297f --- /dev/null +++ b/src/Cluster.php @@ -0,0 +1,108 @@ +streamFactory = $streamFactory ?? new StreamFactory(); + $this->watcher = $watcher ?? new Watcher($this->streamFactory->createStreamCollection()); + $this->initConfiguration($configuration); + } + + /** + * Get string representation of instance. + * @return string String representation + */ + public function __toString(): string + { + return $this->stringable(''); + } + + + + public function createServer(int $port = 80, bool $ssl = false, string|null $name = null): Server + { + $name = $name ?? "server:{$port}"; + if (array_key_exists($name, $this->services)) { + throw new ServerException("Server {$name} already attached."); + } + $server = new Server( + port: $port, + ssl: $ssl, + streamFactory: $this->streamFactory, + watcher: $this->watcher, + configuration: $this->configuration, + ); + $this->services[$name] = $server; + return $server; + } + + /** + * Set logger. + * @param LoggerInterface $logger Logger implementation + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + } + + public function start() + { + foreach ($this->services as $service) { + $service->start(); + } + } +} diff --git a/src/Connection.php b/src/Connection.php index 3d59883..9375a1b 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -197,15 +197,6 @@ public function closeWrite(): self /* ---------- Connection state --------------------------------------------------------------------------------- */ - public function getIdentity(): string - { - return sprintf( - 'connection/%s/%s', - $this->getName() ?: 'closed', - $this->getRemoteName() ?: 'closed', - ); - } - /** * Get name of local socket, or null if not connected. * @return string From bfb18f2dea31d8c06f1be77fbbf4631985f3ff68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Thu, 23 Oct 2025 14:16:10 +0200 Subject: [PATCH 11/16] No. --- src/Cluster.php | 108 ------------------------------------------------ 1 file changed, 108 deletions(-) delete mode 100644 src/Cluster.php diff --git a/src/Cluster.php b/src/Cluster.php deleted file mode 100644 index 45f297f..0000000 --- a/src/Cluster.php +++ /dev/null @@ -1,108 +0,0 @@ -streamFactory = $streamFactory ?? new StreamFactory(); - $this->watcher = $watcher ?? new Watcher($this->streamFactory->createStreamCollection()); - $this->initConfiguration($configuration); - } - - /** - * Get string representation of instance. - * @return string String representation - */ - public function __toString(): string - { - return $this->stringable(''); - } - - - - public function createServer(int $port = 80, bool $ssl = false, string|null $name = null): Server - { - $name = $name ?? "server:{$port}"; - if (array_key_exists($name, $this->services)) { - throw new ServerException("Server {$name} already attached."); - } - $server = new Server( - port: $port, - ssl: $ssl, - streamFactory: $this->streamFactory, - watcher: $this->watcher, - configuration: $this->configuration, - ); - $this->services[$name] = $server; - return $server; - } - - /** - * Set logger. - * @param LoggerInterface $logger Logger implementation - */ - public function setLogger(LoggerInterface $logger): void - { - $this->configuration->setLogger($logger); - } - - public function start() - { - foreach ($this->services as $service) { - $service->start(); - } - } -} From 781c148552a6889b13f983a28eda62395aa7e3ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Tue, 4 Nov 2025 20:55:48 +0100 Subject: [PATCH 12/16] intermediate --- src/Cluster.php | 163 ++++++++++++++++++++++++++ src/Server.php | 80 +++++++------ src/Trait/StringableTrait.php | 2 +- tests/mock/MockStreamTrait.php | 4 +- tests/suites/server/ConfigTest.php | 4 +- tests/suites/server/HandshakeTest.php | 20 ++-- tests/suites/server/ServerTest.php | 38 +++--- 7 files changed, 244 insertions(+), 67 deletions(-) create mode 100644 src/Cluster.php diff --git a/src/Cluster.php b/src/Cluster.php new file mode 100644 index 0000000..9f4b6fb --- /dev/null +++ b/src/Cluster.php @@ -0,0 +1,163 @@ +streamFactory = $streamFactory ?? new StreamFactory(); + $this->watcher = $watcher ?? new Watcher($this->streamFactory->createStreamCollection()); + $this->initConfiguration($configuration); + } + + /** + * Get string representation of instance. + * @return string String representation + */ + public function __toString(): string + { + return $this->stringable(); + } + + public function createServer(int $port = 80, bool $ssl = false, string|null $name = null): Server + { + $name = $name ?? "server:{$port}"; + if (array_key_exists($name, $this->services)) { + throw new ServerException("Server {$name} already attached."); + } + $server = new Server( + port: $port, + ssl: $ssl, + streamFactory: $this->streamFactory, + watcher: $this->watcher, + configuration: $this->configuration, + ); + $this->services[$name] = $server; + return $server; + } + + /** + * Set logger. + * @param LoggerInterface $logger Logger implementation + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + } + + public function start(int|float|null $timeout = null) + { + // Check if running + if ($this->running) { + $this->configuration->getLogger()->warning("[CLUSTER] Cluster is already running"); + return; + } + $this->beforeStart(); + $this->running = true; + $this->configuration->getLogger()->info("[CLUSTER] Cluster is running"); + + // Run handler + while ($this->running) { + try { + $this->beforeWatch(); + if ($this->watcher->isEmpty()) { + $this->stop(); + return; + } + $this->watcher->select($timeout ?? $this->configuration->getTimeout()); + $this->afterWatch(); + + } catch (ExceptionInterface $e) { + // Low-level error + $this->configuration->getLogger()->error("[CLUSTER] {$e->getMessage()}", ['exception' => $e]); +// $this->dispatch('error', [$this, null, $e]); + } catch (Throwable $e) { + // Crash it + $this->configuration->getLogger()->error("[CLUSTER] {$e->getMessage()}", ['exception' => $e]); +// $this->disconnect(); + throw $e; + } + gc_collect_cycles(); // Collect garbage + } + } + + public function stop(): void + { + $this->running = false; + $this->configuration->getLogger()->info("[CLUSTER] Cluster is stopped"); + } + + private function beforeStart(): void + { + foreach ($this->services as $service) { + $service->beforeStart(); + } + } + + private function beforeWatch(): void + { + foreach ($this->services as $service) { + $service->beforeWatch(); + } + } + + private function afterWatch(): void + { + foreach ($this->services as $service) { + $service->afterWatch(); + } + } +} diff --git a/src/Server.php b/src/Server.php index 1bace76..9e62ae5 100644 --- a/src/Server.php +++ b/src/Server.php @@ -76,6 +76,7 @@ class Server implements LoggerAwareInterface, Stringable private StreamFactory $streamFactory; private HttpFactory $httpFactory; private Watcher $watcher; + private string $identity; /* ---------- Magic methods ------------------------------------------------------------------------------------ */ @@ -106,6 +107,7 @@ public function __construct( $this->httpFactory = $httpFactory ?? new DefaultHttpFactory(); $this->watcher = $watcher ?? new Watcher($this->streamFactory->createStreamCollection()); $this->initConfiguration($configuration); + $this->identity = "server:{$port}"; } /** @@ -332,40 +334,32 @@ public function send(Message $message): Message */ public function start(int|float|null $timeout = null): void { - // Create socket server - if (empty($this->server)) { - $this->createSocketServer(); - } - // Check if running if ($this->running) { - $this->configuration->getLogger()->warning("[server] Server is already running"); + $this->configuration->getLogger()->warning("[{$this->identity}] Server is already running"); return; } + $this->beforeStart(); $this->running = true; - $this->configuration->getLogger()->info("[server] Server is running"); + $this->configuration->getLogger()->info("[{$this->identity}] Server is running"); // Run handler while ($this->running) { try { - // Clear closed connections - $this->detachUnconnected(); + $this->beforeWatch(); if ($this->watcher->isEmpty()) { $this->stop(); return; } $this->watcher->watch($timeout ?? $this->configuration->getTimeout()); - foreach ($this->connections as $connection) { - $connection->tick(); - } - $this->dispatch('tick', [$this]); + $this->afterWatch(); } catch (ExceptionInterface $e) { // Low-level error - $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{$this->identity}] {$e->getMessage()}", ['exception' => $e]); $this->dispatch('error', [$this, null, $e]); } catch (Throwable $e) { // Crash it - $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{$this->identity}] {$e->getMessage()}", ['exception' => $e]); $this->disconnect(); throw $e; } @@ -379,7 +373,7 @@ public function start(int|float|null $timeout = null): void public function stop(): void { $this->running = false; - $this->configuration->getLogger()->info("[server] Server is stopped"); + $this->configuration->getLogger()->info("[{$this->identity}] Server is stopped"); } /** @@ -400,7 +394,7 @@ private function selectHandler(string $key, SocketStream $stream): void $this->dispatch($message->getOpcode(), [$this, $connection, $message]); } catch (MessageLevelInterface $e) { // Error, but keep connection open - $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{$this->identity}] {$e->getMessage()}", ['exception' => $e]); $this->dispatch('error', [$this, $connection, $e]); } catch (ConnectionLevelInterface $e) { // Error, disconnect connection @@ -409,14 +403,14 @@ private function selectHandler(string $key, SocketStream $stream): void unset($this->connections[$key]); $connection->disconnect(); } - $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{$this->identity}] {$e->getMessage()}", ['exception' => $e]); $this->dispatch('error', [$this, $connection, $e]); } catch (CloseException $e) { // Should close if ($connection) { $connection->close($e->getCloseStatus(), $e->getMessage()); } - $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{$this->identity}] {$e->getMessage()}", ['exception' => $e]); $this->dispatch('error', [$this, $connection, $e]); } } @@ -430,7 +424,7 @@ private function selectHandler(string $key, SocketStream $stream): void */ public function shutdown(int $closeStatus = 1001): void { - $this->configuration->getLogger()->info('[server] Shutting down'); + $this->configuration->getLogger()->info('[{$this->identity}] Shutting down'); if ($this->getConnectionCount() == 0) { $this->disconnect(); return; @@ -459,7 +453,7 @@ public function shutdown(int $closeStatus = 1001): void public function disconnect(): void { $this->running = false; - $this->watcher->detach('@server'); + $this->watcher->detach($this->identity); foreach ($this->connections as $connection) { $connection->disconnect(); $this->dispatch('disconnect', [$this, $connection]); @@ -469,7 +463,7 @@ public function disconnect(): void $this->server->close(); } $this->server = null; - $this->configuration->getLogger()->info('[server] Server disconnected'); + $this->configuration->getLogger()->info("[{$this->identity}] Server disconnected"); } @@ -482,18 +476,18 @@ protected function createSocketServer(): void $uri = new Uri("{$this->scheme}://0.0.0.0:{$this->port}"); $this->server = $this->streamFactory->createSocketServer($uri, $this->configuration->getContext()); /** @throws StreamException */ - $this->watcher->attach('@server', $this->server, function (string $key, SocketServer $socket) { + $this->watcher->attach($this->identity, $this->server, function (string $key, SocketServer $socket) { $this->acceptHandler($socket); }); $this->allowConnections = true; - $this->configuration->getLogger()->info("[server] Starting server on {$uri}."); + $this->configuration->getLogger()->info("[{$this->identity}] Starting server on {$uri}."); } catch (StreamException $e) { $error = "Server failed to start: {$e->getMessage()}"; - $this->configuration->getLogger()->error("[server] {$error}"); + $this->configuration->getLogger()->error("[{$this->identity}] {$error}"); throw new ServerException($error); } catch (Throwable $e) { $error = "Server error: {$e->getMessage()}"; - $this->configuration->getLogger()->error("[server] {$error}"); + $this->configuration->getLogger()->error("[{$this->identity}] {$error}"); throw $e; } } @@ -504,19 +498,19 @@ protected function acceptHandler(SocketServer $socket): void $maxConnections = $this->configuration->getMaxConnections(); if (!is_null($maxConnections) && $this->getConnectionCount() >= $maxConnections) { $this->configuration->getLogger()->warning( - "[server] Denied connection, reached max {$maxConnections}" + "[{$this->identity}] Denied connection, reached max {$maxConnections}" ); return; } if (!$this->allowConnections) { - $this->configuration->getLogger()->warning("[server] Denied connection, shutting down"); + $this->configuration->getLogger()->warning("[{$this->identity}] Denied connection, shutting down"); return; } try { /** @var SocketStream $stream */ $stream = $socket->accept(); $name = $stream->getRemoteName() ?? 'unknown'; - $this->watcher->attach($name, $stream, function ($key, $stream) { + $this->watcher->attach($name, $stream, function (string $key, SocketStream $stream) { $this->selectHandler($key, $stream); }); $connection = new Connection( @@ -537,7 +531,7 @@ protected function acceptHandler(SocketServer $socket): void /** @throws StreamException */ $request = $this->performHandshake($connection); $this->connections[$name] = $connection; - $this->configuration->getLogger()->info("[server] Accepted connection from {$name}."); + $this->configuration->getLogger()->info("[{$this->identity}] Accepted connection from {$name}."); $this->dispatch('handshake', [ $this, $connection, @@ -551,6 +545,26 @@ protected function acceptHandler(SocketServer $socket): void } } + public function beforeStart(): void + { + if (empty($this->server)) { + $this->createSocketServer(); + } + } + + public function beforeWatch(): void + { + $this->detachUnconnected(); + } + + public function afterWatch(): void + { + foreach ($this->connections as $connection) { + $connection->tick(); + } + $this->dispatch('tick', [$this]); + } + // Detach connections no longer available protected function detachUnconnected(): void { @@ -558,7 +572,7 @@ protected function detachUnconnected(): void if (!$connection->isConnected()) { $this->watcher->detach($key); unset($this->connections[$key]); - $this->configuration->getLogger()->info("[server] Disconnected {$key}."); + $this->configuration->getLogger()->info("[{$this->identity}] Disconnected {$key}."); $this->dispatch('disconnect', [$this, $connection]); } } @@ -623,7 +637,7 @@ protected function performHandshake(Connection $connection): ServerRequest ->withHeader('Connection', 'Upgrade') ->withHeader('Sec-WebSocket-Accept', $responseKey); } catch (HandshakeException $e) { - $this->configuration->getLogger()->warning("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->warning("[{$this->identity}] {$e->getMessage()}", ['exception' => $e]); $response = $e->getResponse(); $exception = $e; } @@ -639,7 +653,7 @@ protected function performHandshake(Connection $connection): ServerRequest throw $exception; } - $this->configuration->getLogger()->debug("[server] Handshake on {$request->getUri()->getPath()}"); + $this->configuration->getLogger()->debug("[{$this->identity}] Handshake on {$request->getUri()->getPath()}"); $connection->setHandshakeRequest($request); $connection->setHandshakeResponse($response); diff --git a/src/Trait/StringableTrait.php b/src/Trait/StringableTrait.php index ba67f1d..8cba7c8 100644 --- a/src/Trait/StringableTrait.php +++ b/src/Trait/StringableTrait.php @@ -18,7 +18,7 @@ public function __toString(): string return get_class($this); } - protected function stringable(string $format, mixed ...$values): string + protected function stringable(string $format = '', mixed ...$values): string { return sprintf("%s({$format})", get_class($this), ...$values); } diff --git a/tests/mock/MockStreamTrait.php b/tests/mock/MockStreamTrait.php index 96a9467..0be7444 100644 --- a/tests/mock/MockStreamTrait.php +++ b/tests/mock/MockStreamTrait.php @@ -189,8 +189,8 @@ private function expectWsServerSetup(string $scheme = 'tcp', int $port = 8000, a } $this->expectSocketServerGetMetadata(); - $this->expectStreamCollectionAttach()->addAssert(function ($method, $params) { - $this->assertEquals('@server', $params[1]); + $this->expectStreamCollectionAttach()->addAssert(function ($method, $params) use ($port) { + $this->assertEquals("server:{$port}", $params[1]); }); } diff --git a/tests/suites/server/ConfigTest.php b/tests/suites/server/ConfigTest.php index df9eef5..38ae940 100644 --- a/tests/suites/server/ConfigTest.php +++ b/tests/suites/server/ConfigTest.php @@ -103,7 +103,7 @@ public function testServerConfiguration(): void $this->assertSame($server, $server->setStreamFactory(new StreamFactory())); $this->expectWsServerSetup(scheme: 'ssl', port: 9000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:9000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); @@ -175,7 +175,7 @@ public function testContext(): void $this->assertSame($context, $server->getContext()); $this->expectWsServerSetup(context: ['ssl' => ['verify_peer' => false]]); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); diff --git a/tests/suites/server/HandshakeTest.php b/tests/suites/server/HandshakeTest.php index 0b40424..23c5ce5 100644 --- a/tests/suites/server/HandshakeTest.php +++ b/tests/suites/server/HandshakeTest.php @@ -60,7 +60,7 @@ public function testHandshakeRequest(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -91,7 +91,7 @@ public function testHandshakeRequestVariant(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -150,7 +150,7 @@ public function testHandshakeRequestFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -185,7 +185,7 @@ public function testHandshakeMethodFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -239,7 +239,7 @@ public function testHandshakeConnectionHeaderFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -293,7 +293,7 @@ public function testHandshakeUpgradeHeaderFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -347,7 +347,7 @@ public function testHandshakeVersionHeaderFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -401,7 +401,7 @@ public function testHandshakeWebSocketKeyHeaderFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -452,7 +452,7 @@ public function testHandshakeWebSocketKeyInvalidFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -506,7 +506,7 @@ public function testHandshakeResponseFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); diff --git a/tests/suites/server/ServerTest.php b/tests/suites/server/ServerTest.php index d795c62..80bc1e8 100644 --- a/tests/suites/server/ServerTest.php +++ b/tests/suites/server/ServerTest.php @@ -140,7 +140,7 @@ public function testListeners(): void }); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); @@ -258,7 +258,7 @@ public function testMiddlewares(): void $server->addMiddleware(new Callback()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); @@ -294,7 +294,7 @@ public function testBroadcastSend(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); @@ -358,7 +358,7 @@ public function testDetachConnection(): void }); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); @@ -379,7 +379,7 @@ public function testDetachConnection(): void $this->expectSocketStreamIsConnected(); $this->expectStreamCollectionDetach(); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); @@ -421,7 +421,7 @@ public function testShutdown(): void $this->expectWsServerSetup(scheme: 'tcp', port: 8000); // Accept connection 1 - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -439,7 +439,7 @@ public function testShutdown(): void $this->expectSocketStreamIsConnected(); // Accept connection 2 - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -471,8 +471,8 @@ public function testShutdown(): void $this->expectSocketStreamIsConnected(); $this->expectSocketStreamIsConnected(); - // The @server handler should be blocked now - $this->expectWsSelectConnections(['@server', 'fake-connection-1', 'fake-connection-2']); + // The server handler should be blocked now + $this->expectWsSelectConnections(['server:8000', 'fake-connection-1', 'fake-connection-2']); // Receive close ack connection 1 $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { @@ -551,7 +551,7 @@ public function testAlreadyStarted(): void }); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); @@ -622,7 +622,7 @@ public function testServerAccessError(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); throw new StreamException(StreamException::SERVER_ACCEPT_ERR, ['uri' => 'test']); @@ -642,7 +642,7 @@ public function testRunBadOpcodeException(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); @@ -690,7 +690,7 @@ public function testRunConnectionClosedException(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); @@ -737,7 +737,7 @@ public function testRunServerException(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); @@ -785,7 +785,7 @@ public function testRunExternalException(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); @@ -830,7 +830,7 @@ public function testUnmaskedException(): void }); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); @@ -872,7 +872,7 @@ public function testMaxConnectionsOverflow(): void $server->setMaxConnections(1); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); $this->expectSocketServerAccept(); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -888,7 +888,7 @@ public function testMaxConnectionsOverflow(): void $this->expectSocketStreamSetTimeout(); $this->expectWsServerPerformHandshake(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['@server'])->addAssert(function () use ($server) { + $this->expectWsSelectConnections(['server:8000'])->addAssert(function () use ($server) { $server->stop(); }); @@ -917,7 +917,7 @@ public function testUnresolvableError(): void }); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server:8000']); // Accept connection $this->expectSocketServerAccept(); $this->expectSocketStream(); From aa7ae606ab3554865a3d658c1458cdf9fac24070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Fri, 7 Nov 2025 09:23:31 +0100 Subject: [PATCH 13/16] Exceptions improvement (#137) Exceptions v4 --- docs/Changelog.md | 1 + docs/ErrorHandling.md | 19 +- phpunit.xml | 1 + src/Client.php | 42 ++- src/Connection.php | 13 +- src/Exception/AbstractConnectionException.php | 42 +++ src/Exception/AbstractException.php | 27 ++ src/Exception/AbstractHandlerException.php | 37 ++ src/Exception/AbstractMessageException.php | 19 + src/Exception/BadOpcodeException.php | 7 +- src/Exception/BadUriException.php | 7 +- src/Exception/ClientException.php | 2 +- src/Exception/CloseException.php | 9 +- src/Exception/ConnectionClosedException.php | 7 +- src/Exception/ConnectionFailureException.php | 7 +- src/Exception/ConnectionLevelInterface.php | 5 + src/Exception/ConnectionTimeoutException.php | 7 +- .../{Exception.php => ControlInterface.php} | 8 +- src/Exception/ExceptionInterface.php | 2 +- src/Exception/HandlerLevelInterface.php | 19 + src/Exception/HandshakeException.php | 15 +- src/Exception/MessageEncodingException.php | 3 +- src/Exception/ReconnectException.php | 8 +- src/Exception/ServerException.php | 2 +- src/Middleware/FollowRedirect.php | 4 +- src/Middleware/SubprotocolNegotiation.php | 2 +- src/Runtime/HandlerInterface.php | 15 + src/Server.php | 40 +- tests/suites/client/ClientTest.php | 6 +- tests/suites/connection/ConnectionTest.php | 17 +- tests/suites/connection/ExceptionTest.php | 8 +- tests/suites/exception/ExceptionTest.php | 357 ++++++++++++++++++ tests/suites/server/ServerTest.php | 6 +- 33 files changed, 668 insertions(+), 96 deletions(-) create mode 100644 src/Exception/AbstractConnectionException.php create mode 100644 src/Exception/AbstractException.php create mode 100644 src/Exception/AbstractHandlerException.php create mode 100644 src/Exception/AbstractMessageException.php rename src/Exception/{Exception.php => ControlInterface.php} (52%) create mode 100644 src/Exception/HandlerLevelInterface.php create mode 100644 src/Runtime/HandlerInterface.php create mode 100644 tests/suites/exception/ExceptionTest.php diff --git a/docs/Changelog.md b/docs/Changelog.md index 54aef85..07f04df 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -11,6 +11,7 @@ * Shared stream observer, sharable across multiple cients and servers (@sirn-se) * Configuration class for various settings (@sirn-se) * MessageEncodingException when compression fails (@sirn-se) + * General improvement of exceptions (@sirn-se) * Many class local configuration setters removed (@sirn-se) * Remove deprecated code (@sirn-se) diff --git a/docs/ErrorHandling.md b/docs/ErrorHandling.md index 9404fc3..b3458f3 100644 --- a/docs/ErrorHandling.md +++ b/docs/ErrorHandling.md @@ -11,9 +11,9 @@ All internal errors has namespace `WebSocket\Exception` and implement `Exception Indicates that a message could not be sent or received. The message affected is dropped as invalid but the connection will remain open for future send and receive actions. -All exceptions on this level implement MessageLevelInterface. +All exceptions on this level implement `MessageLevelInterface`. -- `BadOpcodeException` - When a message received has an invalid opcode +- `BadOpcodeException` - When a message has an invalid opcode - `ConnectionTimeoutException` - Read or write operation on stream has timed out (see timeout configuration) - `MessageEncodingException` - When message content could not be encoded or decoded, typically refers to compressed messages @@ -21,22 +21,29 @@ All exceptions on this level implement MessageLevelInterface. Indicates that the connection has failed, closed or must be closed for some reason. The connection is no longer usable, but could possibly be reconnected. -All exceptions on this level implement ConnectionLevelInterface. +All exceptions on this level implement `ConnectionLevelInterface`. - `ConnectionClosedException` - The underlying stream has closed - `ConnectionFailureException` - The underlying stream has failed for unknown reason - `HandshakeException` - The handshake procedure has failed -### Root errors +### Handler errors These errors indicate some critical error that can not be recovered. +All exceptions on this level implement `HandlerLevelInterface`. - `BadUriException` - The provided URI is invalid - `ClientException` - The Client failed to connect to server -- `CloseException` - Connection is open but must be closed without any messages sent or received -- `ReconnectException` - Connection is open but must reconnect - `ServerException` - The Server failed to open server socket +### Control flow exceptions + +These exceptions indicate an out-of-flow action must be performed. +All exceptions on this level implement `ControlLevelInterface`. + +- `CloseException` - Connection must close +- `ReconnectException` - Connection must reconnect + ## Additional exceptions These standard exceptions typically indicate a configuration error. diff --git a/phpunit.xml b/phpunit.xml index dd92aa0..3037b2a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -16,6 +16,7 @@ tests/suites/client tests/suites/configuration tests/suites/connection + tests/suites/exception tests/suites/frame tests/suites/http tests/suites/message diff --git a/src/Client.php b/src/Client.php index 6f11d0b..a93b564 100644 --- a/src/Client.php +++ b/src/Client.php @@ -46,13 +46,16 @@ SendMethodsTrait, StringableTrait }; -use WebSocket\Runtime\Watcher; +use WebSocket\Runtime\{ + HandlerInterface, + Watcher, +}; /** * WebSocket\Client class. * Entry class for WebSocket client. */ -class Client implements LoggerAwareInterface, Stringable +class Client implements HandlerInterface, LoggerAwareInterface, Stringable { use ConfigurationTrait; /** @use ListenerTrait */ @@ -404,6 +407,11 @@ public function isRunning(): bool /* ---------- Connection management ---------------------------------------------------------------------------- */ + public function getConnection(): Connection|null + { + return $this->connection; + } + /** * If Client has active connection. * @return bool True if active connection. @@ -444,7 +452,7 @@ public function connect(): void ->withScheme(match ($this->socketUri->getScheme()) { 'ws', 'http' => 'tcp', 'wss', 'https' => 'ssl', - default => throw new ClientException("Invalid socket scheme: {$this->socketUri->getScheme()}") + default => throw new ClientException($this, "Invalid socket scheme: {$this->socketUri->getScheme()}") }) ->withHost($this->socketUri->getHost(Uri::IDN_ENCODE)) ->withPort($this->socketUri->getPort(Uri::REQUIRE_PORT)); @@ -463,7 +471,7 @@ public function connect(): void 'message' => $e->getMessage(), 'scope' => 'client', ]); - throw new ClientException($error); + throw new ClientException($this, $error, $e); } $name = $stream->getRemoteName() ?? 'unknown'; $this->watcher->attach($name, $stream, function (string $key, SocketStream $stream) { @@ -475,7 +483,8 @@ public function connect(): void false, $hostUri->getScheme() === 'ssl', $this->httpFactory, - $this->configuration + $this->configuration, + $this ); foreach ($this->middlewares as $middleware) { $this->connection->addMiddleware($middleware); @@ -488,7 +497,7 @@ public function connect(): void 'scope' => 'client', ]); $this->disconnect(); - throw new ClientException($error); + throw new ClientException($this, $error); } try { if (!$this->configuration->isPersistent() || $stream->tell() == 0) { @@ -605,13 +614,18 @@ protected function performHandshake(Uri $uri, Connection $connection): ResponseI $response = $connection->pullHttp(); if ($response->getStatusCode() != 101) { - throw new HandshakeException("Invalid status code {$response->getStatusCode()}.", $response); + throw new HandshakeException( + $connection, + $response, + "Invalid status code {$response->getStatusCode()}.", + ); } if (empty($response->getHeaderLine('Sec-WebSocket-Accept'))) { throw new HandshakeException( + $connection, + $response, "Connection to '{$uri}' failed: Server sent invalid upgrade response.", - $response ); } @@ -621,7 +635,11 @@ protected function performHandshake(Uri $uri, Connection $connection): ResponseI ); if ($responseKey !== $expectedKey) { - throw new HandshakeException("Server sent bad upgrade response.", $response); + throw new HandshakeException( + $connection, + $response, + "Server sent bad upgrade response.", + ); } } catch (HandshakeException $e) { $this->configuration->getLogger()->error("[{scope}] {message}", [ @@ -672,15 +690,15 @@ protected function parseUri(UriInterface|string $uri): Uri $uriInstance = new Uri($uri); } } catch (InvalidArgumentException $e) { - throw new BadUriException("Invalid URI '{$uri}' provided."); + throw new BadUriException($this, "Invalid URI '{$uri}' provided."); } if (!in_array($uriInstance->getScheme(), ['ws', 'wss'])) { - throw new BadUriException("Invalid URI scheme, must be 'ws' or 'wss'."); + throw new BadUriException($this, "Invalid URI scheme, must be 'ws' or 'wss'."); } if (!$uriInstance->getHost()) { - throw new BadUriException("Invalid URI host."); + throw new BadUriException($this, "Invalid URI host."); } return $uriInstance; } diff --git a/src/Connection.php b/src/Connection.php index 9375a1b..d64e551 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -37,6 +37,7 @@ MiddlewareHandler, MiddlewareInterface }; +use WebSocket\Runtime\HandlerInterface; use WebSocket\Trait\{ ConfigurationTrait, SendMethodsTrait, @@ -53,6 +54,7 @@ class Connection implements Stringable use SendMethodsTrait; use StringableTrait; + private HandlerInterface|null $handler; private SocketStream $stream; private HttpHandler $httpHandler; private MessageHandler $messageHandler; @@ -74,7 +76,9 @@ public function __construct( bool $ssl = false, HttpFactory|null $httpFactory = null, Configuration|null $configuration = null, + HandlerInterface|null $handler = null, ) { + $this->handler = $handler; $this->stream = $stream; $this->initConfiguration($configuration); $this->httpHandler = new HttpHandler($this->stream, $ssl, $httpFactory); @@ -93,6 +97,11 @@ public function __construct( $this->stream->setTimeout($this->configuration->getTimeout()); } + public function getHandler(): HandlerInterface|null + { + return $this->handler; + } + public function __toString(): string { return $this->stringable('%s:%s', $this->localName, $this->remoteName); @@ -381,7 +390,7 @@ protected function throwException(Throwable $e): never 'meta' => $meta, 'scope' => 'connection', ]); - throw new ConnectionClosedException(); + throw new ConnectionClosedException($this, null, $e); } } $this->configuration->getLogger()->error("[{scope}] {message}", [ @@ -389,6 +398,6 @@ protected function throwException(Throwable $e): never 'message' => $e->getMessage(), 'scope' => 'connection', ]); - throw new ConnectionFailureException(); + throw new ConnectionFailureException($this, null, $e); } } diff --git a/src/Exception/AbstractConnectionException.php b/src/Exception/AbstractConnectionException.php new file mode 100644 index 0000000..c052344 --- /dev/null +++ b/src/Exception/AbstractConnectionException.php @@ -0,0 +1,42 @@ +connection = $connection; + parent::__construct($message, $previous); + } + + public function getHandler(): HandlerInterface|null + { + return $this->getConnection()?->getHandler(); + } + + public function getConnection(): Connection|null + { + return $this->connection; + } +} diff --git a/src/Exception/AbstractException.php b/src/Exception/AbstractException.php new file mode 100644 index 0000000..a5da6f9 --- /dev/null +++ b/src/Exception/AbstractException.php @@ -0,0 +1,27 @@ +handler = $handler; + parent::__construct($message, $previous); + } + + public function getHandler(): HandlerInterface|null + { + return $this->handler; + } +} diff --git a/src/Exception/AbstractMessageException.php b/src/Exception/AbstractMessageException.php new file mode 100644 index 0000000..32a0693 --- /dev/null +++ b/src/Exception/AbstractMessageException.php @@ -0,0 +1,19 @@ +status = $status; - parent::__construct($content); + parent::__construct($message); } public function getCloseStatus(): int diff --git a/src/Exception/ConnectionClosedException.php b/src/Exception/ConnectionClosedException.php index bac2240..f3bc394 100644 --- a/src/Exception/ConnectionClosedException.php +++ b/src/Exception/ConnectionClosedException.php @@ -11,10 +11,7 @@ * WebSocket\Exception\ConnectionClosedException class. * Connection is unexpectedly closed exception. */ -class ConnectionClosedException extends Exception implements ConnectionLevelInterface +class ConnectionClosedException extends AbstractConnectionException { - public function __construct() - { - parent::__construct('Connection has unexpectedly closed'); - } + protected static string $defaultMessage = 'Connection has unexpectedly closed'; } diff --git a/src/Exception/ConnectionFailureException.php b/src/Exception/ConnectionFailureException.php index 86d3971..815536a 100644 --- a/src/Exception/ConnectionFailureException.php +++ b/src/Exception/ConnectionFailureException.php @@ -11,10 +11,7 @@ * WebSocket\Exception\ConnectionFailureException class. * Unspecified error on connection. */ -class ConnectionFailureException extends Exception implements ConnectionLevelInterface +class ConnectionFailureException extends AbstractConnectionException { - public function __construct(string|null $message = null) - { - parent::__construct($message ?? 'Connection error'); - } + protected static string $defaultMessage = 'Connection error'; } diff --git a/src/Exception/ConnectionLevelInterface.php b/src/Exception/ConnectionLevelInterface.php index e123785..442f9b7 100644 --- a/src/Exception/ConnectionLevelInterface.php +++ b/src/Exception/ConnectionLevelInterface.php @@ -7,10 +7,15 @@ namespace WebSocket\Exception; +use WebSocket\Connection; +use WebSocket\Runtime\HandlerInterface; + /** * WebSocket\Exception\ConnectionLevelInterface interface. * Indicates error on connection level - connection should be closed */ interface ConnectionLevelInterface extends ExceptionInterface { + public function getHandler(): HandlerInterface|null; + public function getConnection(): Connection|null; } diff --git a/src/Exception/ConnectionTimeoutException.php b/src/Exception/ConnectionTimeoutException.php index 0fa21bc..dffa3b0 100644 --- a/src/Exception/ConnectionTimeoutException.php +++ b/src/Exception/ConnectionTimeoutException.php @@ -11,10 +11,7 @@ * WebSocket\Exception\ConnectionTimeoutException class. * Connection operation has timed out. */ -class ConnectionTimeoutException extends Exception implements MessageLevelInterface +class ConnectionTimeoutException extends AbstractMessageException { - public function __construct() - { - parent::__construct('Connection operation timeout'); - } + protected static string $defaultMessage = 'Connection operation timeout'; } diff --git a/src/Exception/Exception.php b/src/Exception/ControlInterface.php similarity index 52% rename from src/Exception/Exception.php rename to src/Exception/ControlInterface.php index 5e81e05..0f208b5 100644 --- a/src/Exception/Exception.php +++ b/src/Exception/ControlInterface.php @@ -7,12 +7,10 @@ namespace WebSocket\Exception; -use RuntimeException; - /** - * WebSocket\Exception\Exception abstract class. - * Core exception for repo + * WebSocket\Exception\ControlInterface interface. + * Indicates that handler should perform some actio0n. */ -abstract class Exception extends RuntimeException implements ExceptionInterface +interface ControlInterface extends ExceptionInterface { } diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php index 30d06d8..593b12d 100644 --- a/src/Exception/ExceptionInterface.php +++ b/src/Exception/ExceptionInterface.php @@ -15,5 +15,5 @@ */ interface ExceptionInterface extends Throwable { - public function getMessage(): string; + //public function getMessage(): string; } diff --git a/src/Exception/HandlerLevelInterface.php b/src/Exception/HandlerLevelInterface.php new file mode 100644 index 0000000..330c401 --- /dev/null +++ b/src/Exception/HandlerLevelInterface.php @@ -0,0 +1,19 @@ +response = $response; + parent::__construct($connection, $message); } public function getResponse(): ResponseInterface diff --git a/src/Exception/MessageEncodingException.php b/src/Exception/MessageEncodingException.php index 4907fb3..5db730b 100644 --- a/src/Exception/MessageEncodingException.php +++ b/src/Exception/MessageEncodingException.php @@ -11,6 +11,7 @@ * WebSocket\Exception\MessageEncodingException class. * Message content could not be encoded/decoded. */ -class MessageEncodingException extends Exception implements MessageLevelInterface +class MessageEncodingException extends AbstractMessageException { + protected static string $defaultMessage = 'Message encoding error'; } diff --git a/src/Exception/ReconnectException.php b/src/Exception/ReconnectException.php index 4bec3ac..3bbe0b7 100644 --- a/src/Exception/ReconnectException.php +++ b/src/Exception/ReconnectException.php @@ -13,14 +13,16 @@ * WebSocket\Exception\ReconnectException class. * Reconnect requested. */ -class ReconnectException extends Exception +class ReconnectException extends AbstractException implements ControlInterface { + protected static string $defaultMessage = 'Reconnect connection'; + private Uri|null $uri; - public function __construct(Uri|null $uri = null) + public function __construct(Uri|null $uri = null, string|null $message = null) { $this->uri = $uri; - parent::__construct("Reconnect requested" . ($uri ? ": {$uri}" : '')); + parent::__construct($message); } public function getUri(): Uri|null diff --git a/src/Exception/ServerException.php b/src/Exception/ServerException.php index 43d69f5..40e9222 100644 --- a/src/Exception/ServerException.php +++ b/src/Exception/ServerException.php @@ -11,6 +11,6 @@ * WebSocket\Exception\ServerException class. * Fatal exception during server start */ -class ServerException extends Exception +class ServerException extends AbstractHandlerException { } diff --git a/src/Middleware/FollowRedirect.php b/src/Middleware/FollowRedirect.php index 8506df9..7b7499f 100644 --- a/src/Middleware/FollowRedirect.php +++ b/src/Middleware/FollowRedirect.php @@ -59,13 +59,13 @@ public function processHttpIncoming(ProcessHttpStack $stack, Connection $connect $note = "{$this->attempts} of {$this->limit} redirect attempts"; if ($this->attempts > $this->limit) { $this->configuration->getLogger()->debug("[follow-redirect] Too many redirect attempts, giving up"); - throw new HandshakeException("Too many redirect attempts, giving up", $message); + throw new HandshakeException($connection, $message, "Too many redirect attempts, giving up"); } $this->attempts++; $this->configuration->getLogger()->debug( "[follow-redirect] {$message->getStatusCode()} {$locationHeader} ($note)" ); - throw new ReconnectException(new Uri($locationHeader)); + throw new ReconnectException(new Uri($locationHeader), "Reconnect requested: {$locationHeader}"); } return $message; } diff --git a/src/Middleware/SubprotocolNegotiation.php b/src/Middleware/SubprotocolNegotiation.php index a74695b..c9925ec 100644 --- a/src/Middleware/SubprotocolNegotiation.php +++ b/src/Middleware/SubprotocolNegotiation.php @@ -109,7 +109,7 @@ public function processHttpIncoming(ProcessHttpStack $stack, Connection $connect } elseif ($this->require) { // No matching subprotocol, close and fail $connection->close(); - throw new HandshakeException("Could not resolve subprotocol.", $message); + throw new HandshakeException($connection, $message, "Could not resolve subprotocol."); } } return $message; diff --git a/src/Runtime/HandlerInterface.php b/src/Runtime/HandlerInterface.php new file mode 100644 index 0000000..652c2ad --- /dev/null +++ b/src/Runtime/HandlerInterface.php @@ -0,0 +1,15 @@ + */ @@ -490,7 +493,7 @@ protected function createSocketServer(): void } catch (StreamException $e) { $error = "Server failed to start: {$e->getMessage()}"; $this->configuration->getLogger()->error("[server] {$error}"); - throw new ServerException($error); + throw new ServerException($this, $error, $e); } catch (Throwable $e) { $error = "Server error: {$e->getMessage()}"; $this->configuration->getLogger()->error("[server] {$error}"); @@ -525,10 +528,11 @@ protected function acceptHandler(SocketServer $socket): void true, $this->isSsl(), $this->httpFactory, - $this->configuration + $this->configuration, + $this ); } catch (StreamException $e) { - throw new ConnectionFailureException("Server failed to accept: {$e->getMessage()}"); + throw new ConnectionFailureException(null, "Server failed to accept: {$e->getMessage()}", $e); } try { foreach ($this->middlewares as $middleware) { @@ -547,7 +551,7 @@ protected function acceptHandler(SocketServer $socket): void $this->dispatch('connect', [$this, $connection, $request]); } catch (ExceptionInterface | StreamException $e) { $connection->disconnect(); - throw new ConnectionFailureException("Server failed to accept: {$e->getMessage()}"); + throw new ConnectionFailureException($connection, "Server failed to accept: {$e->getMessage()}", $e); } } @@ -578,42 +582,48 @@ protected function performHandshake(Connection $connection): ServerRequest try { if ($request->getMethod() != 'GET') { throw new HandshakeException( + $connection, + $response->withStatus(405), "Handshake request with invalid method: '{$request->getMethod()}'", - $response->withStatus(405) ); } $connectionHeader = trim($request->getHeaderLine('Connection')); if (!str_contains(strtolower($connectionHeader), 'upgrade')) { throw new HandshakeException( + $connection, + $response->withStatus(426), "Handshake request with invalid Connection header: '{$connectionHeader}'", - $response->withStatus(426) ); } $upgradeHeader = trim($request->getHeaderLine('Upgrade')); if (strtolower($upgradeHeader) != 'websocket') { throw new HandshakeException( + $connection, + $response->withStatus(426), "Handshake request with invalid Upgrade header: '{$upgradeHeader}'", - $response->withStatus(426) ); } $versionHeader = trim($request->getHeaderLine('Sec-WebSocket-Version')); if ($versionHeader != '13') { throw new HandshakeException( + $connection, + $response->withStatus(426)->withHeader('Sec-WebSocket-Version', '13'), "Handshake request with invalid Sec-WebSocket-Version header: '{$versionHeader}'", - $response->withStatus(426)->withHeader('Sec-WebSocket-Version', '13') ); } $keyHeader = trim($request->getHeaderLine('Sec-WebSocket-Key')); if (empty($keyHeader)) { throw new HandshakeException( + $connection, + $response->withStatus(426), "Handshake request with invalid Sec-WebSocket-Key header: '{$keyHeader}'", - $response->withStatus(426) ); } if (strlen(base64_decode($keyHeader)) != 16) { throw new HandshakeException( + $connection, + $response->withStatus(426), "Handshake request with invalid Sec-WebSocket-Key header: '{$keyHeader}'", - $response->withStatus(426) ); } @@ -632,7 +642,11 @@ protected function performHandshake(Connection $connection): ServerRequest /** @var Response */ $response = $connection->pushHttp($response); if ($response->getStatusCode() != 101) { - $exception = new HandshakeException("Invalid status code {$response->getStatusCode()}", $response); + $exception = new HandshakeException( + $connection, + $response, + "Invalid status code {$response->getStatusCode()}", + ); } if ($exception) { diff --git a/tests/suites/client/ClientTest.php b/tests/suites/client/ClientTest.php index ff66171..46d2d93 100644 --- a/tests/suites/client/ClientTest.php +++ b/tests/suites/client/ClientTest.php @@ -126,6 +126,8 @@ public function testClientSendReceive(): void $this->expectSocketStreamIsConnected(); $this->assertTrue($client->isConnected()); + $this->assertInstanceOf(Connection::class, $client->getConnection()); + $this->assertSame($client, $client->getConnection()->getHandler()); $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); @@ -1030,8 +1032,8 @@ public function testRunClientException(): void $this->expectWsClientSelectConnections(['localhost:8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); - })->setReturn(function () { - throw new ClientException(); + })->setReturn(function () use ($client) { + throw new ClientException($client, 'Test error'); }); $this->expectStreamCollectionDetach(); $this->expectSocketStreamIsConnected(); diff --git a/tests/suites/connection/ConnectionTest.php b/tests/suites/connection/ConnectionTest.php index bc84f25..8ae6200 100644 --- a/tests/suites/connection/ConnectionTest.php +++ b/tests/suites/connection/ConnectionTest.php @@ -70,6 +70,7 @@ public function testCreate(): void $connection = new Connection($stream, false, false); $this->assertInstanceOf(Connection::class, $connection); $this->assertInstanceOf(Stringable::class, $connection); + $this->assertNull($connection->getHandler()); $this->expectSocketStreamIsConnected(); $this->assertTrue($connection->isConnected()); @@ -198,8 +199,8 @@ public function testSendHttpError(): void $this->expectWsConnectionCreate(); $connection = new Connection($stream, false, false); - $this->expectSocketStreamWrite()->setReturn(function () { - throw new ConnectionClosedException(); + $this->expectSocketStreamWrite()->setReturn(function () use ($connection) { + throw new ConnectionClosedException($connection); }); $this->expectException(ConnectionClosedException::class); $this->expectExceptionMessage('Connection has unexpectedly closed'); @@ -220,8 +221,8 @@ public function testPullHttpError(): void $this->expectWsConnectionCreate(); $connection = new Connection($stream, false, false); - $this->expectSocketStreamReadLine()->setReturn(function () { - throw new ConnectionClosedException(); + $this->expectSocketStreamReadLine()->setReturn(function () use ($connection) { + throw new ConnectionClosedException($connection); }); $this->expectException(ConnectionClosedException::class); $this->expectExceptionMessage('Connection has unexpectedly closed'); @@ -242,8 +243,8 @@ public function testSendMessageError(): void $this->expectWsConnectionCreate(); $connection = new Connection($stream, false, false); - $this->expectSocketStreamWrite()->setReturn(function () { - throw new ConnectionClosedException(); + $this->expectSocketStreamWrite()->setReturn(function () use ($connection) { + throw new ConnectionClosedException($connection); }); $this->expectException(ConnectionClosedException::class); $this->expectExceptionMessage('Connection has unexpectedly closed'); @@ -264,8 +265,8 @@ public function testPullMessageError(): void $this->expectWsConnectionCreate(); $connection = new Connection($stream, false, false); - $this->expectSocketStreamRead()->setReturn(function () { - throw new ConnectionClosedException(); + $this->expectSocketStreamRead()->setReturn(function () use ($connection) { + throw new ConnectionClosedException($connection); }); $this->expectException(ConnectionClosedException::class); $this->expectExceptionMessage('Connection has unexpectedly closed'); diff --git a/tests/suites/connection/ExceptionTest.php b/tests/suites/connection/ExceptionTest.php index 635cde5..d45b400 100644 --- a/tests/suites/connection/ExceptionTest.php +++ b/tests/suites/connection/ExceptionTest.php @@ -102,8 +102,8 @@ public function testConnectionClosedException(): void $this->expectWsConnectionCreate(); $connection = new Connection($stream, false, false); - $this->expectSocketStreamWrite()->setReturn(function () { - throw new ConnectionClosedException(); + $this->expectSocketStreamWrite()->setReturn(function () use ($connection) { + throw new ConnectionClosedException($connection); }); $this->expectException(ConnectionClosedException::class); $this->expectExceptionMessage('Connection has unexpectedly closed'); @@ -123,8 +123,8 @@ public function testConnectionFailureException(): void $this->expectWsConnectionCreate(); $connection = new Connection($stream, false, false); - $this->expectSocketStreamWrite()->setReturn(function () { - throw new ConnectionFailureException(); + $this->expectSocketStreamWrite()->setReturn(function () use ($connection) { + throw new ConnectionFailureException($connection); }); $this->expectException(ConnectionFailureException::class); $this->expectExceptionMessage('Connection error'); diff --git a/tests/suites/exception/ExceptionTest.php b/tests/suites/exception/ExceptionTest.php new file mode 100644 index 0000000..277b70e --- /dev/null +++ b/tests/suites/exception/ExceptionTest.php @@ -0,0 +1,357 @@ +setUpStack(); + } + + public function tearDown(): void + { + $this->tearDownStack(); + } + + public function testClientException(): void + { + $client = new Client('ws://localhost:8000'); + + try { + throw new ClientException(); + } catch (ClientException $e) { + ; + } + $this->assertInstanceOf(ClientException::class, $e); + $this->assertInstanceOf(AbstractHandlerException::class, $e); + $this->assertInstanceOf(AbstractException::class, $e); + $this->assertInstanceOf(HandlerLevelInterface::class, $e); + $this->assertInstanceOf(ExceptionInterface::class, $e); + $this->assertEquals('Unspecified handler error', $e->getMessage()); + $this->assertNull($e->getHandler()); + + try { + throw new ClientException($client, 'Test message'); + } catch (ClientException $e) { + ; + } + $this->assertEquals('Test message', $e->getMessage()); + $this->assertSame($client, $e->getHandler()); + } + + public function testServerException(): void + { + $server = new Server(); + + try { + throw new ServerException(); + } catch (ServerException $e) { + ; + } + $this->assertInstanceOf(ServerException::class, $e); + $this->assertInstanceOf(AbstractHandlerException::class, $e); + $this->assertInstanceOf(AbstractException::class, $e); + $this->assertInstanceOf(HandlerLevelInterface::class, $e); + $this->assertInstanceOf(ExceptionInterface::class, $e); + $this->assertEquals('Unspecified handler error', $e->getMessage()); + $this->assertNull($e->getHandler()); + + try { + throw new ServerException($server, 'Test message'); + } catch (ServerException $e) { + ; + } + $this->assertEquals('Test message', $e->getMessage()); + $this->assertSame($server, $e->getHandler()); + } + + public function testBadUriException(): void + { + $client = new Client('ws://localhost:8000'); + + try { + throw new BadUriException($client); + } catch (BadUriException $e) { + ; + } + $this->assertInstanceOf(BadUriException::class, $e); + $this->assertInstanceOf(AbstractHandlerException::class, $e); + $this->assertInstanceOf(AbstractException::class, $e); + $this->assertInstanceOf(HandlerLevelInterface::class, $e); + $this->assertInstanceOf(ExceptionInterface::class, $e); + $this->assertEquals('Bad URI', $e->getMessage()); + $this->assertSame($client, $e->getHandler()); + + try { + throw new BadUriException($client, 'Test message'); + } catch (BadUriException $e) { + ; + } + $this->assertEquals('Test message', $e->getMessage()); + } + + public function testConnectionClosedException(): void + { + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); + $this->expectWsClientConnect(); + $this->expectWsClientPerformHandshake(); + $client->connect(); + $connection = $client->getConnection(); + + try { + throw new ConnectionClosedException(); + } catch (ConnectionClosedException $e) { + ; + } + $this->assertInstanceOf(ConnectionClosedException::class, $e); + $this->assertInstanceOf(AbstractConnectionException::class, $e); + $this->assertInstanceOf(AbstractException::class, $e); + $this->assertInstanceOf(ConnectionLevelInterface::class, $e); + $this->assertInstanceOf(ExceptionInterface::class, $e); + $this->assertEquals('Connection has unexpectedly closed', $e->getMessage()); + $this->assertNull($e->getConnection()); + $this->assertNull($e->getHandler()); + + try { + throw new ConnectionClosedException($connection, 'Test message'); + } catch (ConnectionClosedException $e) { + ; + } + $this->assertEquals('Test message', $e->getMessage()); + $this->assertSame($connection, $e->getConnection()); + $this->assertSame($client, $e->getHandler()); + } + + public function testConnectionFailureException(): void + { + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); + $this->expectWsClientConnect(); + $this->expectWsClientPerformHandshake(); + $client->connect(); + /** @var Connection $connection */ + $connection = $client->getConnection(); + + try { + throw new ConnectionFailureException(); + } catch (ConnectionFailureException $e) { + ; + } + $this->assertInstanceOf(ConnectionFailureException::class, $e); + $this->assertInstanceOf(AbstractConnectionException::class, $e); + $this->assertInstanceOf(AbstractException::class, $e); + $this->assertInstanceOf(ConnectionLevelInterface::class, $e); + $this->assertInstanceOf(ExceptionInterface::class, $e); + $this->assertEquals('Connection error', $e->getMessage()); + $this->assertNull($e->getConnection()); + $this->assertNull($e->getHandler()); + + try { + throw new ConnectionFailureException($connection, 'Test message'); + } catch (ConnectionFailureException $e) { + ; + } + $this->assertEquals('Test message', $e->getMessage()); + $this->assertSame($connection, $e->getConnection()); + $this->assertSame($client, $e->getHandler()); + } + + public function testHandshakeException(): void + { + $this->expectWsClientCreate(); + $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); + $this->expectWsClientConnect(); + $this->expectWsClientPerformHandshake(); + $client->connect(); + /** @var Connection $connection */ + $connection = $client->getConnection(); + $response = new Response(); + + try { + throw new HandshakeException($connection, $response); + } catch (HandshakeException $e) { + ; + } + $this->assertInstanceOf(HandshakeException::class, $e); + $this->assertInstanceOf(AbstractConnectionException::class, $e); + $this->assertInstanceOf(AbstractException::class, $e); + $this->assertInstanceOf(ConnectionLevelInterface::class, $e); + $this->assertInstanceOf(ExceptionInterface::class, $e); + $this->assertEquals('Handshake failed', $e->getMessage()); + $this->assertSame($connection, $e->getConnection()); + $this->assertSame($client, $e->getHandler()); + $this->assertSame($response, $e->getResponse()); + + try { + throw new HandshakeException($connection, $response, 'Test message'); + } catch (HandshakeException $e) { + ; + } + $this->assertEquals('Test message', $e->getMessage()); + } + + public function testBadOpcodeException(): void + { + try { + throw new BadOpcodeException(); + } catch (BadOpcodeException $e) { + ; + } + $this->assertInstanceOf(BadOpcodeException::class, $e); + $this->assertInstanceOf(AbstractMessageException::class, $e); + $this->assertInstanceOf(AbstractException::class, $e); + $this->assertInstanceOf(MessageLevelInterface::class, $e); + $this->assertInstanceOf(ExceptionInterface::class, $e); + $this->assertEquals('Bad Opcode', $e->getMessage()); + + try { + throw new BadOpcodeException('Test message'); + } catch (BadOpcodeException $e) { + ; + } + $this->assertEquals('Test message', $e->getMessage()); + } + + public function testConnectionTimeoutException(): void + { + try { + throw new ConnectionTimeoutException(); + } catch (ConnectionTimeoutException $e) { + ; + } + $this->assertInstanceOf(ConnectionTimeoutException::class, $e); + $this->assertInstanceOf(AbstractMessageException::class, $e); + $this->assertInstanceOf(AbstractException::class, $e); + $this->assertInstanceOf(MessageLevelInterface::class, $e); + $this->assertInstanceOf(ExceptionInterface::class, $e); + $this->assertEquals('Connection operation timeout', $e->getMessage()); + + try { + throw new ConnectionTimeoutException('Test message'); + } catch (ConnectionTimeoutException $e) { + ; + } + $this->assertEquals('Test message', $e->getMessage()); + } + + public function testMessageEncodingException(): void + { + try { + throw new MessageEncodingException(); + } catch (MessageEncodingException $e) { + ; + } + $this->assertInstanceOf(MessageEncodingException::class, $e); + $this->assertInstanceOf(AbstractMessageException::class, $e); + $this->assertInstanceOf(AbstractException::class, $e); + $this->assertInstanceOf(MessageLevelInterface::class, $e); + $this->assertInstanceOf(ExceptionInterface::class, $e); + $this->assertEquals('Message encoding error', $e->getMessage()); + + try { + throw new MessageEncodingException('Test message'); + } catch (MessageEncodingException $e) { + ; + } + $this->assertEquals('Test message', $e->getMessage()); + } + + public function testCloseException(): void + { + try { + throw new CloseException(); + } catch (CloseException $e) { + ; + } + $this->assertInstanceOf(CloseException::class, $e); + $this->assertInstanceOf(AbstractException::class, $e); + $this->assertInstanceOf(ControlInterface::class, $e); + $this->assertInstanceOf(ExceptionInterface::class, $e); + $this->assertEquals(1000, $e->getCloseStatus()); + $this->assertEquals('Closing connection', $e->getMessage()); + + try { + throw new CloseException(1200, 'Test message'); + } catch (CloseException $e) { + ; + } + $this->assertEquals(1200, $e->getCloseStatus()); + $this->assertEquals('Test message', $e->getMessage()); + } + + public function testReconnectException(): void + { + $uri = new Uri('ws://localhost:9000'); + + try { + throw new ReconnectException(); + } catch (ReconnectException $e) { + ; + } + $this->assertInstanceOf(ReconnectException::class, $e); + $this->assertInstanceOf(AbstractException::class, $e); + $this->assertInstanceOf(ControlInterface::class, $e); + $this->assertInstanceOf(ExceptionInterface::class, $e); + $this->assertNull($e->getUri()); + $this->assertEquals('Reconnect connection', $e->getMessage()); + + try { + throw new ReconnectException($uri, 'Test message'); + } catch (ReconnectException $e) { + ; + } + $this->assertEquals($uri, $e->getUri()); + $this->assertEquals('Test message', $e->getMessage()); + } +} diff --git a/tests/suites/server/ServerTest.php b/tests/suites/server/ServerTest.php index d795c62..287c0db 100644 --- a/tests/suites/server/ServerTest.php +++ b/tests/suites/server/ServerTest.php @@ -242,6 +242,10 @@ public function testListeners(): void }); $server->start(); + foreach ($server->getConnections() as $connection) { + $this->assertSame($server, $connection->getHandler()); + } + $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); $this->expectSocketServerClose(); @@ -759,7 +763,7 @@ public function testRunServerException(): void $this->assertEquals(2, $params[0]); })->setReturn(function () use ($server) { $server->stop(); - throw new ServerException(); + throw new ServerException($server, 'Test error'); }); $server->start(); From f72d47b796049bcaac4cd104eac34fa5bf2c4ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Sun, 7 Dec 2025 17:04:14 +0100 Subject: [PATCH 14/16] V4 backport (#140) * Misc typos (#139) * chore: fix markdown linking * chore: typos * Typos, php versions * 8.5 fix * Backport 2.6.1 --------- Co-authored-by: Pieter Oliver <68863060+pieterocp@users.noreply.github.com> --- .gitattributes | 1 + .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/workflows/acceptance.yml | 8 ++++---- .gitignore | 1 - Makefile | 1 - README.md | 1 - composer.json | 6 +++--- docs/Changelog.md | 5 +++++ docs/Client.md | 4 ++-- docs/Contributing.md | 5 +++-- docs/Index.md | 2 +- docs/Middleware/Callback.md | 2 +- docs/Server.md | 4 ++-- examples/send.php | 2 +- phpunit.xml | 10 +--------- tests/mock/MockStreamTrait.php | 4 ++-- 16 files changed, 27 insertions(+), 31 deletions(-) diff --git a/.gitattributes b/.gitattributes index 9130466..8ec3a1a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ .github/ export-ignore .gitignore export-ignore docs/ export-ignore +docu.json export-ignore examples/ export-ignore Makefile export-ignore phpcs.xml export-ignore diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3876c4e..5f0a00c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,7 +14,7 @@ Please provide a clear and concise description of the suspected issue. If possible, provide information - possibly including code snippets - on how to reproduce the issue. **Logs** -If possible, provide logs that indicate the issue. See [logger example](../../docs/Examples.md#logger) on how to use a logger. +If possible, provide logs that indicate the issue. See https://github.com/Textalk/websocket-php/blob/master/docs/Examples.md#logger on how to use a logger. **Versions** * Version of this library diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 3533e8c..ffca2a2 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -7,7 +7,7 @@ jobs: test: strategy: matrix: - php-versions: ["8.1", "8.2", "8.3", "8.4", "8.5"] + php-versions: ["8.1", "8.2", "8.3", "8.4", "8.5", "8.6"] runs-on: ubuntu-latest name: Unit test steps: @@ -43,7 +43,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.4" + php-version: "8.5" coverage: none env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -70,7 +70,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.4" + php-version: "8.5" coverage: pcov env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -97,7 +97,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.4" + php-version: "8.5" coverage: none env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index b3d14cb..7c5ff72 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .DS_Store .phpunit.result.cache -build/ composer.lock composer.phar coverage/ diff --git a/Makefile b/Makefile index 8405a20..e70a299 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,6 @@ vendor/bin/phpunit: install clean: clean-coverage rm -f composer.lock rm -rf vendor - rm -rf coverage clean-coverage: rm -rf coverage diff --git a/README.md b/README.md index f95913e..68a91fe 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ # Websocket Client and Server for PHP [![Build Status](https://github.com/sirn-se/websocket-php/actions/workflows/acceptance.yml/badge.svg)](https://github.com/sirn-se/websocket-php/actions) -[![Coverage Status](https://coveralls.io/repos/github/sirn-se/websocket-php/badge.svg?branch=v2.0-main)](https://coveralls.io/github/sirn-se/websocket-php) This library contains WebSocket client and server for PHP. diff --git a/composer.json b/composer.json index 72874d6..0a70a73 100644 --- a/composer.json +++ b/composer.json @@ -31,13 +31,13 @@ "phrity/http": "^1.0", "phrity/net-uri": "^2.1", "phrity/net-stream": "dev-ws4 as 2.4.0", - "psr/http-message": "^1.1 | ^2.0", - "psr/log": "^1.0 | ^2.0 | ^3.0" + "psr/http-message": "^1.1 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "guzzlehttp/psr7": "^2.0", "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^10.0 | ^11.0 | ^12.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", "phrity/logger-console": "^1.0", "phrity/net-mock": "dev-ws4 as 2.4.0", "phrity/util-errorhandler": "^1.1", diff --git a/docs/Changelog.md b/docs/Changelog.md index 07f04df..02f2362 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -19,6 +19,11 @@ > PHP version `^8.1` +### `3.6.1` + + * Fix typos and broken links (@pieterocp) + * Repo setup, PHP 8.5 default, 8.6 unit test (@sirn-se) + ### `3.6.0` * CloseConnection and ReconnectException in Client listener (@sirn-se) diff --git a/docs/Client.md b/docs/Client.md index 23ae6a7..8fd1165 100644 --- a/docs/Client.md +++ b/docs/Client.md @@ -155,7 +155,7 @@ Read more on [Configuration](Configuration.md). ### HTTP factories By default the Client uses a minimal [PSR-7 HTTP message](https://www.php-fig.org/psr/psr-7/) implementation. -Other (more complete) implementations can be used by setting [PSR-17 HTTP factories](https://www.php-fig.org/psr/psr-17/) on the CLient. +Other (more complete) implementations can be used by setting [PSR-17 HTTP factories](https://www.php-fig.org/psr/psr-17/) on the Client. Set a configured HttpFactory class on the Client. ```php @@ -183,7 +183,7 @@ $client->addHeader("Sec-WebSocket-Protocol", "soap"); ## Connection control -Client will automatically connect when sending a message or starting the listner. +Client will automatically connect when sending a message or starting the listener. You may also connect and disconnect manually. ```php diff --git a/docs/Contributing.md b/docs/Contributing.md index 52a9545..7c816e1 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -12,7 +12,7 @@ Requirements on pull requests; * All tests **MUST** pass. * Code coverage **MUST** remain at 100%. * Code **MUST** adhere to PSR-1 and PSR-12 code standards. -* Staic analysis check **MUST** pass. +* Static analysis check **MUST** pass. ## SemVer, versions, and target branches @@ -103,4 +103,5 @@ make stan * Dejan Levec * Pieter Oliver * Sebastian Hagens -* Adrian Mihai \ No newline at end of file +* Adrian Mihai +* Pieter Oliver diff --git a/docs/Index.md b/docs/Index.md index dc37991..f356646 100644 --- a/docs/Index.md +++ b/docs/Index.md @@ -19,7 +19,7 @@ * [Changelog](Changelog.md) - The changelog of this repo * [Contributing](Contributing.md) - Contributors and requirements * [Examples](Examples.md) - Examples -* [License](../LICENCE.md) - License +* [License](../LICENSE.md) - License * [Class synopsis](Class_Synopsis.md) - Public API of core classes ## Migration diff --git a/docs/Middleware/Callback.md b/docs/Middleware/Callback.md index b34412f..ab35481 100644 --- a/docs/Middleware/Callback.md +++ b/docs/Middleware/Callback.md @@ -32,7 +32,7 @@ $client_or_server )); ``` -* The `incoming` and `outgoing` callbacks **MUST** return a [Message](Message.md) instance +* The `incoming` and `outgoing` callbacks **MUST** return a [Message](../Message.md) instance * The `httpIncoming` and `httpOutgoing` callbacks **MUST** return a HTTP request/response instance respectively * The `tick` callback returns nothing diff --git a/docs/Server.md b/docs/Server.md index 47396d8..c209bb8 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -172,7 +172,7 @@ $server->setHttpFactory(Phrity\Http\HttpFactory::create($psrFactory)); ### Server info -Additional methids that provides information. +Additional methods that provides information. ```php echo "port: {$server->getPort()}\n"; @@ -195,7 +195,7 @@ $server->start(); // Stop server - When called, server will no longer listen to incoming messages but will not disconnect clients $server->stop(); -// Orderly shutdown server - Will initate close procedure on all connected clients and stop running when all are disconnected +// Orderly shutdown server - Will initiate close procedure on all connected clients and stop running when all are disconnected $server->shutdown(); // Disconnect server - Server will immediately stop and disconnect all clients without normal close procedure diff --git a/examples/send.php b/examples/send.php index ac9bfa4..5d80561 100644 --- a/examples/send.php +++ b/examples/send.php @@ -53,7 +53,7 @@ 'opcode' => 'text', ], getopt('', ['uri:', 'opcode:', 'timeout:', 'framesize:', 'deflate'])); -$message = array_pop($argv); +$message = empty($argv) ? 'Test message' : array_pop($argv); // Initiate client. try { diff --git a/phpunit.xml b/phpunit.xml index 3037b2a..742f649 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -13,15 +13,7 @@ > - tests/suites/client - tests/suites/configuration - tests/suites/connection - tests/suites/exception - tests/suites/frame - tests/suites/http - tests/suites/message - tests/suites/middleware - tests/suites/server + tests/suites diff --git a/tests/mock/MockStreamTrait.php b/tests/mock/MockStreamTrait.php index 96a9467..810fe28 100644 --- a/tests/mock/MockStreamTrait.php +++ b/tests/mock/MockStreamTrait.php @@ -36,7 +36,7 @@ trait MockStreamTrait private string $lastWsKey = ''; - /* ---------- WebSocket Client combinded asserts --------------------------------------------------------------- */ + /* ---------- WebSocket Client combined asserts --------------------------------------------------------------- */ private function expectWsClientCreate(): void { @@ -160,7 +160,7 @@ function (string $method, array $params) use ($host, $path, $headers): void { } - /* ---------- WebSocket Server combinded asserts --------------------------------------------------------------- */ + /* ---------- WebSocket Server combined asserts --------------------------------------------------------------- */ private function expectWsServerCreate(): void { From 1c048ded0d0270a34537ffa468c8c1cc3578a749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Sun, 7 Dec 2025 22:20:17 +0100 Subject: [PATCH 15/16] Handler, identity, logging (#142) --- src/Client.php | 121 ++++---- src/Connection.php | 85 ++++-- src/Frame/FrameHandler.php | 6 +- src/Message/MessageHandler.php | 10 +- src/Middleware/CloseHandler.php | 16 +- src/Middleware/CompressionExtension.php | 16 +- src/Middleware/FollowRedirect.php | 15 +- src/Middleware/MiddlewareHandler.php | 31 +- src/Middleware/PingInterval.php | 2 +- src/Middleware/SubprotocolNegotiation.php | 26 +- src/Runtime/HandlerInterface.php | 2 +- src/Runtime/IdentityInterface.php | 19 ++ src/Server.php | 127 ++++++-- src/Trait/StringableTrait.php | 2 +- tests/mock/MockStreamTrait.php | 38 ++- tests/suites/client/ClientErrorTest.php | 11 +- tests/suites/client/ClientTest.php | 34 +-- tests/suites/connection/ConnectionTest.php | 28 +- tests/suites/connection/ExceptionTest.php | 32 +- tests/suites/middleware/CallbackTest.php | 20 +- tests/suites/middleware/CloseHandlerTest.php | 11 +- .../middleware/DeflateCompressorTest.php | 26 +- .../suites/middleware/FollowRedirectTest.php | 11 +- tests/suites/middleware/PingIntervalTest.php | 8 +- tests/suites/middleware/PingResponderTest.php | 8 +- tests/suites/middleware/ProcessStackTest.php | 11 +- .../middleware/SubprotocolNegotiationTest.php | 23 +- tests/suites/server/ConfigTest.php | 34 +-- tests/suites/server/HandshakeTest.php | 140 ++------- tests/suites/server/ServerTest.php | 286 +++--------------- 30 files changed, 553 insertions(+), 646 deletions(-) create mode 100644 src/Runtime/IdentityInterface.php diff --git a/src/Client.php b/src/Client.php index a93b564..dde7afb 100644 --- a/src/Client.php +++ b/src/Client.php @@ -48,6 +48,7 @@ }; use WebSocket\Runtime\{ HandlerInterface, + IdentityInterface, Watcher, }; @@ -55,7 +56,7 @@ * WebSocket\Client class. * Entry class for WebSocket client. */ -class Client implements HandlerInterface, LoggerAwareInterface, Stringable +class Client implements HandlerInterface, IdentityInterface, LoggerAwareInterface, Stringable { use ConfigurationTrait; /** @use ListenerTrait */ @@ -77,6 +78,8 @@ class Client implements HandlerInterface, LoggerAwareInterface, Stringable private StreamFactory $streamFactory; private HttpFactory $httpFactory; private Watcher $watcher; + /** @var non-empty-string $identity */ + private string $identity = 'client/'; /* ---------- Magic methods ------------------------------------------------------------------------------------ */ @@ -100,6 +103,7 @@ public function __construct( $this->httpFactory = $httpFactory ?? new DefaultHttpFactory(); $this->watcher = $watcher ?? new Watcher($this->streamFactory->createStreamCollection()); $this->initConfiguration($configuration); + $this->identity = "client/{$this->socketUri->getHost()}"; } /** @@ -111,6 +115,11 @@ public function __toString(): string return $this->stringable('%s', $this->connection ? $this->socketUri->__toString() : 'closed'); } + public function getIdentity(): string + { + return $this->identity; + } + /* ---------- Configuration ------------------------------------------------------------------------------------ */ @@ -279,15 +288,15 @@ public function start(int|float|null $timeout = null): void { // Check if running if ($this->running) { - $this->configuration->getLogger()->warning("[{scope}] Client is already running", [ - 'scope' => 'client', + $this->configuration->getLogger()->warning('[{identity}] Client is already running', [ + 'identity' => $this->identity, ]); return; } $this->running = true; $reconnect = false; - $this->configuration->getLogger()->info("[{scope}] Client is running", [ - 'scope' => 'client', + $this->configuration->getLogger()->info('[{identity}] Client is running', [ + 'identity' => $this->identity, ]); $connection = $this->connection(); @@ -304,10 +313,10 @@ public function start(int|float|null $timeout = null): void } catch (CloseException $e) { // Close connection $connection->close($e->getCloseStatus(), $e->getMessage()); - $this->configuration->getLogger()->error("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error} (close)', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), - 'scope' => 'client', + 'error' => $e->getMessage(), ]); $this->dispatch('error', [$this, $connection, $e]); } catch (ReconnectException $e) { @@ -317,32 +326,30 @@ public function start(int|float|null $timeout = null): void $this->socketUri = $uri; } $connection->close(); - $this->configuration->getLogger()->error("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error} (reconnect)', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), - 'scope' => 'client', + 'error' => $e->getMessage(), ]); $this->dispatch('error', [$this, $connection, $e]); } catch (ExceptionInterface $e) { $this->disconnect(); $this->running = false; - // Low-level error - $this->configuration->getLogger()->error("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), - 'scope' => 'client', + 'error' => $e->getMessage(), ]); $this->dispatch('error', [$this, null, $e]); } catch (Throwable $e) { $this->disconnect(); $this->running = false; - // Crash it - $this->configuration->getLogger()->error("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), - 'scope' => 'client', + 'error' => $e->getMessage(), ]); throw $e; } @@ -365,33 +372,32 @@ private function selectHandler(string $key, SocketStream $stream): void $this->dispatch($message->getOpcode(), [$this, $connection, $message]); } catch (MessageLevelInterface $e) { // Error, but keep connection open - $this->configuration->getLogger()->error("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error} (message)', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), - 'scope' => 'client', + 'error' => $e->getMessage(), ]); $this->dispatch('error', [$this, $connection, $e]); } catch (ConnectionLevelInterface $e) { // Error, disconnect connection $this->disconnect(); - $this->configuration->getLogger()->error("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error} (connection)', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), - 'scope' => 'client', + 'error' => $e->getMessage(), ]); $this->dispatch('error', [$this, $connection, $e]); } } - /** * Stop client listener (resumable). */ public function stop(): void { $this->running = false; - $this->configuration->getLogger()->info("[{scope}] Client is stopped", [ - 'scope' => 'client', + $this->configuration->getLogger()->info('[{identity}] Client is stopped', [ + 'identity' => $this->identity, ]); } @@ -466,49 +472,49 @@ public function connect(): void $stream = $client->connect(); } catch (Throwable $e) { $error = "Could not open socket to \"{$hostUri}\": {$e->getMessage()}"; - $this->configuration->getLogger()->error("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), - 'scope' => 'client', + 'error' => $error, ]); throw new ClientException($this, $error, $e); } - $name = $stream->getRemoteName() ?? 'unknown'; - $this->watcher->attach($name, $stream, function (string $key, SocketStream $stream) { - $this->selectHandler($key, $stream); - }); $this->connection = new Connection( + $this, $stream, true, false, $hostUri->getScheme() === 'ssl', $this->httpFactory, $this->configuration, - $this ); - foreach ($this->middlewares as $middleware) { - $this->connection->addMiddleware($middleware); - } - if (!$this->isConnected()) { + $this->watcher->attach($this->connection->getIdentity(), $stream, function (string $key, SocketStream $stream) { + $this->selectHandler($key, $stream); + }); + + if (is_null($this->connection) || !$this->isConnected()) { $error = "Invalid stream on \"{$hostUri}\"."; - $this->configuration->getLogger()->error("[{scope}] {message}", [ - 'message' => $error, - 'scope' => 'client', + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, + 'error' => $error, ]); $this->disconnect(); throw new ClientException($this, $error); } + foreach ($this->middlewares as $middleware) { + $this->connection->addMiddleware($middleware); + } try { if (!$this->configuration->isPersistent() || $stream->tell() == 0) { /** @throws ReconnectException */ $response = $this->performHandshake($this->socketUri, $this->connection); } } catch (ReconnectException $e) { - $this->configuration->getLogger()->info("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), - 'scope' => 'client', + 'error' => $e->getMessage(), ]); if ($uri = $e->getUri()) { $this->socketUri = $uri; @@ -516,10 +522,11 @@ public function connect(): void $this->connect(); return; } - $this->configuration->getLogger()->info("[{scope}] Client connected to {$this->socketUri}", [ - 'scope' => 'client', + $this->configuration->getLogger()->info('[{identity}] Client connected to {uri}', [ + 'identity' => $this->identity, 'uri' => $this->socketUri, ]); + $this->dispatch('handshake', [ $this, $this->connection, @@ -535,12 +542,12 @@ public function connect(): void public function disconnect(): void { if ($this->connection) { - $this->watcher->detach($this->connection->getRemoteName()); + $this->watcher->detach($this->connection->getIdentity()); } if ($this->connection && $this->isConnected()) { $this->connection->disconnect(); - $this->configuration->getLogger()->info("[{scope}] Client disconnected", [ - 'scope' => 'client', + $this->configuration->getLogger()->info('[{identity}] Client disconnected', [ + 'identity' => $this->identity, ]); $this->dispatch('disconnect', [$this, $this->connection]); } @@ -642,18 +649,18 @@ protected function performHandshake(Uri $uri, Connection $connection): ResponseI ); } } catch (HandshakeException $e) { - $this->configuration->getLogger()->error("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error} (handshake)', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), - 'scope' => 'client', + 'error' => $e->getMessage(), ]); throw $e; } - - $this->configuration->getLogger()->debug("[{scope}] Handshake on {$uri->getPath()}", [ + $this->configuration->getLogger()->debug('[{identity}] Handshake on {path}', [ + 'identity' => $this->identity, 'path' => $uri->getPath(), - 'scope' => 'client', ]); + $connection->setHandshakeRequest($request); $connection->setHandshakeResponse($response); diff --git a/src/Connection.php b/src/Connection.php index d64e551..7babae2 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -37,7 +37,10 @@ MiddlewareHandler, MiddlewareInterface }; -use WebSocket\Runtime\HandlerInterface; +use WebSocket\Runtime\{ + HandlerInterface, + IdentityInterface, +}; use WebSocket\Trait\{ ConfigurationTrait, SendMethodsTrait, @@ -48,13 +51,13 @@ * WebSocket\Connection class. * A client/server connection, wrapping socket stream. */ -class Connection implements Stringable +class Connection implements IdentityInterface, Stringable { use ConfigurationTrait; use SendMethodsTrait; use StringableTrait; - private HandlerInterface|null $handler; + private HandlerInterface $handler; private SocketStream $stream; private HttpHandler $httpHandler; private MessageHandler $messageHandler; @@ -65,18 +68,20 @@ class Connection implements Stringable private ResponseInterface|null $handshakeResponse = null; /** @var array $meta */ private array $meta = []; + /** @var non-empty-string $identity */ + private string $identity = 'client/'; /* ---------- Magic methods ------------------------------------------------------------------------------------ */ public function __construct( + HandlerInterface $handler, SocketStream $stream, bool $pushMasked, bool $pullMaskedRequired, bool $ssl = false, HttpFactory|null $httpFactory = null, Configuration|null $configuration = null, - HandlerInterface|null $handler = null, ) { $this->handler = $handler; $this->stream = $stream; @@ -93,18 +98,28 @@ public function __construct( ); $this->localName = $this->stream->getLocalName() ?? ''; $this->remoteName = $this->stream->getRemoteName() ?? ''; - + $this->identity = sprintf( + '%s/connection/%s/%s', + $this->handler->getIdentity(), + $this->getIdentityPart($this->localName), + $this->getIdentityPart($this->remoteName), + ); $this->stream->setTimeout($this->configuration->getTimeout()); } - public function getHandler(): HandlerInterface|null + public function __toString(): string + { + return $this->stringable('%s:%s', $this->localName, $this->remoteName); + } + + public function getHandler(): HandlerInterface { return $this->handler; } - public function __toString(): string + public function getIdentity(): string { - return $this->stringable('%s:%s', $this->localName, $this->remoteName); + return $this->identity; } @@ -127,9 +142,9 @@ public function getContext(): Context public function addMiddleware(MiddlewareInterface $middleware): self { $this->middlewareHandler->add($middleware); - $this->configuration->getLogger()->debug("[{scope}] Added middleware: {$middleware}", [ - 'middleware' => $middleware, - 'scope' => 'connection', + $this->configuration->getLogger()->debug('[{identity}] Added middleware: {middleware}', [ + 'identity' => $this->identity, + 'middleware' => (string)$middleware, ]); return $this; } @@ -170,8 +185,8 @@ public function isWritable(): bool */ public function disconnect(): self { - $this->configuration->getLogger()->info("[{scope}] Closing connection", [ - 'scope' => 'connection', + $this->configuration->getLogger()->info('[{identity}] Closing connection', [ + 'identity' => $this->identity, ]); $this->stream->close(); return $this; @@ -183,8 +198,8 @@ public function disconnect(): self */ public function closeRead(): self { - $this->configuration->getLogger()->info("[{scope}] Closing further reading", [ - 'scope' => 'connection', + $this->configuration->getLogger()->info('[{identity}] Closing further reading', [ + 'identity' => $this->identity, ]); $this->stream->closeRead(); return $this; @@ -196,8 +211,8 @@ public function closeRead(): self */ public function closeWrite(): self { - $this->configuration->getLogger()->info("[{scope}] Closing further writing", [ - 'scope' => 'connection', + $this->configuration->getLogger()->info('[{identity}] Closing further writing', [ + 'identity' => $this->identity, ]); $this->stream->closeWrite(); return $this; @@ -355,18 +370,18 @@ protected function throwException(Throwable $e): never { // Internal exceptions are handled and re-thrown if ($e instanceof ReconnectException) { - $this->configuration->getLogger()->info("[{scope}] {message}", [ + $this->configuration->getLogger()->info('[{identity}] {error}', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), - 'scope' => 'connection', + 'error' => $e->getMessage(), ]); throw $e; } if ($e instanceof ExceptionInterface) { - $this->configuration->getLogger()->error("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), - 'scope' => 'connection', + 'error' => $e->getMessage(), ]); throw $e; } @@ -375,29 +390,35 @@ protected function throwException(Throwable $e): never $meta = $this->stream->getMetadata(); $json = json_encode($meta); if (!empty($meta['timed_out'])) { - $this->configuration->getLogger()->error("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), + 'error' => $e->getMessage(), 'meta' => $meta, - 'scope' => 'connection', ]); throw new ConnectionTimeoutException(); } if (!empty($meta['eof'])) { - $this->configuration->getLogger()->error("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), + 'error' => $e->getMessage(), 'meta' => $meta, - 'scope' => 'connection', ]); throw new ConnectionClosedException($this, null, $e); } } - $this->configuration->getLogger()->error("[{scope}] {message}", [ + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, 'exception' => $e, - 'message' => $e->getMessage(), - 'scope' => 'connection', + 'error' => $e->getMessage(), ]); throw new ConnectionFailureException($this, null, $e); } + + protected function getIdentityPart(string $source): string + { + preg_match('/([0-9]+)$/', $source, $result); + return empty($result) ? $source : array_shift($result); + } } diff --git a/src/Frame/FrameHandler.php b/src/Frame/FrameHandler.php index 1e60fd3..f3cbf0e 100644 --- a/src/Frame/FrameHandler.php +++ b/src/Frame/FrameHandler.php @@ -100,14 +100,14 @@ public function pull(): Frame } $frame = new Frame($opcode, $payload, $final, $rsv1, $rsv2, $rsv3); - $this->configuration->getLogger()->debug("[frame-handler] Pulled '{$opcode}' frame", [ + $this->configuration->getLogger()->debug('[frame-handler] Pulled {opcode} frame', [ 'opcode' => $frame->getOpcode(), 'final' => $frame->isFinal(), 'content-length' => $frame->getPayloadLength(), ]); if ($this->pullMaskedRequired && !$masked) { - $this->configuration->getLogger()->error("[frame-handler] Masking required, but frame was unmasked"); + $this->configuration->getLogger()->debug('[frame-handler] Masking required, but frame was unmasked'); throw new CloseException(1002, 'Masking required'); } @@ -162,7 +162,7 @@ public function push(Frame $frame): int // Write to stream. $written = $this->write($data); - $this->configuration->getLogger()->debug("[frame-handler] Pushed '{opcode}' frame", [ + $this->configuration->getLogger()->debug('[frame-handler] Pushed {opcode} frame', [ 'opcode' => $frame->getOpcode(), 'final' => $frame->isFinal(), 'content-length' => $frame->getPayloadLength(), diff --git a/src/Message/MessageHandler.php b/src/Message/MessageHandler.php index 67269dd..7ee2d35 100644 --- a/src/Message/MessageHandler.php +++ b/src/Message/MessageHandler.php @@ -53,12 +53,11 @@ public function push(Message $message, int $size = self::DEFAULT_SIZE): Message foreach ($frames as $frame) { $this->frameHandler->push($frame); } - $this->configuration->getLogger()->info("[{scope}] Pushed {message}", [ + $this->configuration->getLogger()->info('[message-handler] Pushed {message}', [ 'content-length' => $message->getLength(), 'frames' => count($frames), - 'message' => $message, + 'message' => (string)$message, 'opcode' => $message->getOpcode(), - 'scope' => 'message-handler', ]); return $message; } @@ -101,12 +100,11 @@ private function createMessage(array $frames): Message return $carry . $item->getPayload(); }, '')); $message->setCompress($frames[0]->getRsv1() ?? false); - $this->configuration->getLogger()->info("[{scope}] Pulled {message}", [ + $this->configuration->getLogger()->info('[message-handler] Pulled {message}', [ 'content-length' => $message->getLength(), 'frames' => count($frames), - 'message' => $message, + 'message' => (string)$message, 'opcode' => $message->getOpcode(), - 'scope' => 'message-handler', ]); return $message; } diff --git a/src/Middleware/CloseHandler.php b/src/Middleware/CloseHandler.php index 79ba01d..28d0279 100644 --- a/src/Middleware/CloseHandler.php +++ b/src/Middleware/CloseHandler.php @@ -40,15 +40,15 @@ public function processIncoming(ProcessStack $stack, Connection $connection): Me } if ($connection->isWritable()) { // Remote sent Close; acknowledge and close for further reading - $this->configuration->getLogger()->debug( - "[close-handler] Received 'close', status: {$message->getCloseStatus()}" - ); + $this->configuration->getLogger()->debug('[close-handler] Received \'close\', status: {status}', [ + 'status' => $message->getCloseStatus(), + ]); $ack = "Close acknowledged: {$message->getCloseStatus()}"; $connection->closeRead(); $connection->send(new Close($message->getCloseStatus(), $ack)); } else { // Remote sent Close/Ack: disconnect - $this->configuration->getLogger()->debug("[close-handler] Received 'close' acknowledge, disconnecting"); + $this->configuration->getLogger()->debug('[close-handler] Received \'close\', acknowledge, disconnecting'); $connection->disconnect(); } return $message; @@ -62,13 +62,13 @@ public function processOutgoing(ProcessStack $stack, Connection $connection, Mes } if ($connection->isReadable()) { // Local sent Close: close for further writing, expect remote acknowledge - $this->configuration->getLogger()->debug( - "[close-handler] Sent 'close', status: {$message->getCloseStatus()}" - ); + $this->configuration->getLogger()->debug('[close-handler] Sent \'close\', status: {status}', [ + 'status' => $message->getCloseStatus(), + ]); $connection->closeWrite(); } else { // Local sent Close/Ack: disconnect - $this->configuration->getLogger()->debug("[close-handler] Sent 'close' acknowledge, disconnecting"); + $this->configuration->getLogger()->debug('[close-handler] Sent \'close\', acknowledge, disconnecting'); $connection->disconnect(); } return $message; diff --git a/src/Middleware/CompressionExtension.php b/src/Middleware/CompressionExtension.php index 68cba13..45e4d0b 100644 --- a/src/Middleware/CompressionExtension.php +++ b/src/Middleware/CompressionExtension.php @@ -89,20 +89,20 @@ public function processHttpIncoming(ProcessHttpStack $stack, Connection $connect if ($preferred = $this->getPreferred($message)) { $connection->setMeta('compressionExtension.compressor', $preferred->compressor); $connection->setMeta('compressionExtension.configuration', $preferred->configuration); - $this->configuration->getLogger()->debug( - "[permessage-compression] Using {$preferred->compressor}", - (array)$preferred->configuration - ); + $this->configuration->getLogger()->debug('[permessage-compression] Using {compressor}', [ + 'compressor' => $preferred->compressor, + 'configuration' => (array)$preferred->configuration, + ]); } } elseif ($message instanceof ResponseInterface) { // Incoming Response on Client if ($preferred = $this->getPreferred($message)) { $connection->setMeta('compressionExtension.compressor', $preferred->compressor); $connection->setMeta('compressionExtension.configuration', $preferred->configuration); - $this->configuration->getLogger()->debug( - "[permessage-compression] Using {$preferred->compressor}", - (array)$preferred->configuration - ); + $this->configuration->getLogger()->debug('[permessage-compression] Using {compressor}', [ + 'compressor' => $preferred->compressor, + 'configuration' => (array)$preferred->configuration, + ]); } // @todo: If not found? } diff --git a/src/Middleware/FollowRedirect.php b/src/Middleware/FollowRedirect.php index 7b7499f..e3182e4 100644 --- a/src/Middleware/FollowRedirect.php +++ b/src/Middleware/FollowRedirect.php @@ -56,15 +56,20 @@ public function processHttpIncoming(ProcessHttpStack $stack, Connection $connect && $message->getStatusCode() < 400 && $locationHeader = $message->getHeaderLine('Location') ) { - $note = "{$this->attempts} of {$this->limit} redirect attempts"; if ($this->attempts > $this->limit) { - $this->configuration->getLogger()->debug("[follow-redirect] Too many redirect attempts, giving up"); + $this->configuration->getLogger()->warning('[follow-redirect] Too many redirect attempts, giving up', [ + 'attempts' => $this->attempts, + 'limit' => $this->limit, + ]); throw new HandshakeException($connection, $message, "Too many redirect attempts, giving up"); } $this->attempts++; - $this->configuration->getLogger()->debug( - "[follow-redirect] {$message->getStatusCode()} {$locationHeader} ($note)" - ); + $this->configuration->getLogger()->debug('[follow-redirect] Redirect {status} {location}', [ + 'status' => $message->getStatusCode(), + 'location' => $locationHeader, + 'attempts' => $this->attempts, + 'limit' => $this->limit, + ]); throw new ReconnectException(new Uri($locationHeader), "Reconnect requested: {$locationHeader}"); } return $message; diff --git a/src/Middleware/MiddlewareHandler.php b/src/Middleware/MiddlewareHandler.php index a2603bf..42c74a7 100644 --- a/src/Middleware/MiddlewareHandler.php +++ b/src/Middleware/MiddlewareHandler.php @@ -70,23 +70,34 @@ public function __construct( public function add(MiddlewareInterface $middleware): self { if ($middleware instanceof ProcessIncomingInterface) { - $this->configuration->getLogger()->info("[middleware-handler] Added incoming: {$middleware}"); + $this->configuration->getLogger()->debug('[middleware-handler] Added incoming: {middleware}', [ + 'middleware' => (string)$middleware, + ]); $this->incoming[] = $middleware; } if ($middleware instanceof ProcessOutgoingInterface) { - $this->configuration->getLogger()->info("[middleware-handler] Added outgoing: {$middleware}"); + $this->configuration->getLogger()->debug('[middleware-handler] Added outgoing: {middleware}', [ + 'middleware' => (string)$middleware, + ]); + $this->outgoing[] = $middleware; } if ($middleware instanceof ProcessHttpIncomingInterface) { - $this->configuration->getLogger()->info("[middleware-handler] Added http incoming: {$middleware}"); + $this->configuration->getLogger()->debug('[middleware-handler] Added http incoming: {middleware}', [ + 'middleware' => (string)$middleware, + ]); $this->httpIncoming[] = $middleware; } if ($middleware instanceof ProcessHttpOutgoingInterface) { - $this->configuration->getLogger()->info("[middleware-handler] Added http outgoing: {$middleware}"); + $this->configuration->getLogger()->debug('[middleware-handler] Added http outgoing: {middleware}', [ + 'middleware' => (string)$middleware, + ]); $this->httpOutgoing[] = $middleware; } if ($middleware instanceof ProcessTickInterface) { - $this->configuration->getLogger()->info("[middleware-handler] Added tick: {$middleware}"); + $this->configuration->getLogger()->debug('[middleware-handler] Added tick: {middleware}', [ + 'middleware' => (string)$middleware, + ]); $this->tick[] = $middleware; } $middleware->setConfiguration($this->configuration); @@ -100,7 +111,7 @@ public function add(MiddlewareInterface $middleware): self */ public function processIncoming(Connection $connection): Message { - $this->configuration->getLogger()->info("[middleware-handler] Processing incoming"); + $this->configuration->getLogger()->debug('[middleware-handler] Processing incoming'); $stack = new ProcessStack($connection, $this->messageHandler, $this->incoming); return $stack->handleIncoming(); } @@ -114,7 +125,7 @@ public function processIncoming(Connection $connection): Message */ public function processOutgoing(Connection $connection, Message $message): Message { - $this->configuration->getLogger()->info("[middleware-handler] Processing outgoing"); + $this->configuration->getLogger()->debug('[middleware-handler] Processing outgoing'); $stack = new ProcessStack($connection, $this->messageHandler, $this->outgoing); return $stack->handleOutgoing($message); } @@ -126,7 +137,7 @@ public function processOutgoing(Connection $connection, Message $message): Messa */ public function processHttpIncoming(Connection $connection): MessageInterface { - $this->configuration->getLogger()->info("[middleware-handler] Processing http incoming"); + $this->configuration->getLogger()->debug('[middleware-handler] Processing http incoming'); $stack = new ProcessHttpStack($connection, $this->httpHandler, $this->httpIncoming); return $stack->handleHttpIncoming(); } @@ -139,7 +150,7 @@ public function processHttpIncoming(Connection $connection): MessageInterface */ public function processHttpOutgoing(Connection $connection, MessageInterface $message): MessageInterface { - $this->configuration->getLogger()->info("[middleware-handler] Processing http outgoing"); + $this->configuration->getLogger()->debug('[middleware-handler] Processing http outgoing'); $stack = new ProcessHttpStack($connection, $this->httpHandler, $this->httpOutgoing); return $stack->handleHttpOutgoing($message); } @@ -150,7 +161,7 @@ public function processHttpOutgoing(Connection $connection, MessageInterface $me */ public function processTick(Connection $connection): void { - $this->configuration->getLogger()->info("[middleware-handler] Processing tick"); + $this->configuration->getLogger()->debug('[middleware-handler] Processing tick'); $stack = new ProcessTickStack($connection, $this->tick); $stack->handleTick(); } diff --git a/src/Middleware/PingInterval.php b/src/Middleware/PingInterval.php index cb2739e..a220b26 100644 --- a/src/Middleware/PingInterval.php +++ b/src/Middleware/PingInterval.php @@ -45,7 +45,7 @@ public function processTick(ProcessTickStack $stack, Connection $connection): vo { // Push if time exceeds timestamp for next ping if ($connection->isWritable() && microtime(true) >= $this->getNext($connection)) { - $this->configuration->getLogger()->debug("[ping-interval] Auto-pushing ping"); + $this->configuration->getLogger()->debug('[ping-interval] Auto-pushing ping'); $connection->send(new Ping()); $this->setNext($connection); // Update timestamp for next ping } diff --git a/src/Middleware/SubprotocolNegotiation.php b/src/Middleware/SubprotocolNegotiation.php index c9925ec..c21c05a 100644 --- a/src/Middleware/SubprotocolNegotiation.php +++ b/src/Middleware/SubprotocolNegotiation.php @@ -58,15 +58,17 @@ public function processHttpOutgoing( $message = $message->withAddedHeader('Sec-WebSocket-Protocol', $subprotocol); } if ($supported = implode(', ', $this->subprotocols)) { - $this->configuration->getLogger()->debug( - "[subprotocol-negotiation] Requested subprotocols: {$supported}" - ); + $this->configuration->getLogger()->debug('[subprotocol-negotiation] Requested: {subprotocols}', [ + 'subprotocols' => $supported, + ]); } } elseif ($message instanceof ResponseInterface) { // Outgoing Response on Server if ($selected = $connection->getMeta('subprotocolNegotiation.selected')) { $message = $message->withHeader('Sec-WebSocket-Protocol', $selected); - $this->configuration->getLogger()->info("[subprotocol-negotiation] Selected subprotocol: {$selected}"); + $this->configuration->getLogger()->info('[subprotocol-negotiation] Selected: {subprotocol}', [ + 'subprotocol' => $selected, + ]); } elseif ($this->require) { // No matching subprotocol, fail handshake $message = $message->withStatus(426); @@ -86,14 +88,14 @@ public function processHttpIncoming(ProcessHttpStack $stack, Connection $connect if ($message instanceof ServerRequestInterface) { // Incoming requests on Server if ($requested = $message->getHeaderLine('Sec-WebSocket-Protocol')) { - $this->configuration->getLogger()->debug( - "[subprotocol-negotiation] Requested subprotocols: {$requested}" - ); + $this->configuration->getLogger()->debug('[subprotocol-negotiation] Requested: {subprotocols}', [ + 'subprotocols' => $requested, + ]); } if ($supported = implode(', ', $this->subprotocols)) { - $this->configuration->getLogger()->debug( - "[subprotocol-negotiation] Supported subprotocols: {$supported}" - ); + $this->configuration->getLogger()->debug('[subprotocol-negotiation] Supported: {subprotocols}', [ + 'subprotocols' => $supported, + ]); } foreach ($message->getHeader('Sec-WebSocket-Protocol') as $subprotocol) { if (in_array($subprotocol, $this->subprotocols)) { @@ -105,7 +107,9 @@ public function processHttpIncoming(ProcessHttpStack $stack, Connection $connect // Incoming Response on Client if ($selected = $message->getHeaderLine('Sec-WebSocket-Protocol')) { $connection->setMeta('subprotocolNegotiation.selected', $selected); - $this->configuration->getLogger()->info("[subprotocol-negotiation] Selected subprotocol: {$selected}"); + $this->configuration->getLogger()->info('[subprotocol-negotiation] Selected: {subprotocol}', [ + 'subprotocol' => $selected, + ]); } elseif ($this->require) { // No matching subprotocol, close and fail $connection->close(); diff --git a/src/Runtime/HandlerInterface.php b/src/Runtime/HandlerInterface.php index 652c2ad..4a76a94 100644 --- a/src/Runtime/HandlerInterface.php +++ b/src/Runtime/HandlerInterface.php @@ -10,6 +10,6 @@ /** * WebSocket\Runtime\HandlerInterface interface. */ -interface HandlerInterface +interface HandlerInterface extends IdentityInterface { } diff --git a/src/Runtime/IdentityInterface.php b/src/Runtime/IdentityInterface.php new file mode 100644 index 0000000..b6c6919 --- /dev/null +++ b/src/Runtime/IdentityInterface.php @@ -0,0 +1,19 @@ + */ @@ -79,6 +80,8 @@ class Server implements HandlerInterface, LoggerAwareInterface, Stringable private StreamFactory $streamFactory; private HttpFactory $httpFactory; private Watcher $watcher; + /** @var non-empty-string $identity */ + private string $identity; /* ---------- Magic methods ------------------------------------------------------------------------------------ */ @@ -109,6 +112,7 @@ public function __construct( $this->httpFactory = $httpFactory ?? new DefaultHttpFactory(); $this->watcher = $watcher ?? new Watcher($this->streamFactory->createStreamCollection()); $this->initConfiguration($configuration); + $this->identity = "server/{$port}"; } /** @@ -120,6 +124,11 @@ public function __toString(): string return $this->stringable('%s', $this->server ? "{$this->scheme}://0.0.0.0:{$this->port}" : 'closed'); } + public function getIdentity(): string + { + return $this->identity; + } + /* ---------- Configuration ------------------------------------------------------------------------------------ */ @@ -342,11 +351,15 @@ public function start(int|float|null $timeout = null): void // Check if running if ($this->running) { - $this->configuration->getLogger()->warning("[server] Server is already running"); + $this->configuration->getLogger()->warning('[{identity}] Server is already running', [ + 'identity' => $this->identity, + ]); return; } $this->running = true; - $this->configuration->getLogger()->info("[server] Server is running"); + $this->configuration->getLogger()->info('[{identity}] Server is running', [ + 'identity' => $this->identity, + ]); // Run handler while ($this->running) { @@ -364,11 +377,19 @@ public function start(int|float|null $timeout = null): void $this->dispatch('tick', [$this]); } catch (ExceptionInterface $e) { // Low-level error - $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, + 'exception' => $e, + 'error' => $e->getMessage(), + ]); $this->dispatch('error', [$this, null, $e]); } catch (Throwable $e) { // Crash it - $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, + 'exception' => $e, + 'error' => $e->getMessage(), + ]); $this->disconnect(); throw $e; } @@ -382,7 +403,9 @@ public function start(int|float|null $timeout = null): void public function stop(): void { $this->running = false; - $this->configuration->getLogger()->info("[server] Server is stopped"); + $this->configuration->getLogger()->info('[{identity}] Server is stopped', [ + 'identity' => $this->identity, + ]); } /** @@ -403,23 +426,36 @@ private function selectHandler(string $key, SocketStream $stream): void $this->dispatch($message->getOpcode(), [$this, $connection, $message]); } catch (MessageLevelInterface $e) { // Error, but keep connection open - $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error('[{identity}] {error} (message)', [ + 'identity' => $this->identity, + 'exception' => $e, + 'error' => $e->getMessage(), + ]); $this->dispatch('error', [$this, $connection, $e]); } catch (ConnectionLevelInterface $e) { // Error, disconnect connection if ($connection) { - $this->watcher->detach($key); + $this->watcher->detach($connection->getIdentity()); unset($this->connections[$key]); $connection->disconnect(); } - $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error('[{identity}] {error} (connection)', [ + 'identity' => $this->identity, + 'exception' => $e, + 'error' => $e->getMessage(), + ]); $this->dispatch('error', [$this, $connection, $e]); } catch (CloseException $e) { // Should close if ($connection) { $connection->close($e->getCloseStatus(), $e->getMessage()); } - $this->configuration->getLogger()->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->info('[{identity}] {error} (close)', [ + 'identity' => $this->identity, + 'exception' => $e, + 'error' => $e->getMessage(), + 'status' => $e->getCloseStatus(), + ]); $this->dispatch('error', [$this, $connection, $e]); } } @@ -433,7 +469,9 @@ private function selectHandler(string $key, SocketStream $stream): void */ public function shutdown(int $closeStatus = 1001): void { - $this->configuration->getLogger()->info('[server] Shutting down'); + $this->configuration->getLogger()->info('[{identity}] Shutting down', [ + 'identity' => $this->identity, + ]); if ($this->getConnectionCount() == 0) { $this->disconnect(); return; @@ -462,7 +500,7 @@ public function shutdown(int $closeStatus = 1001): void public function disconnect(): void { $this->running = false; - $this->watcher->detach('@server'); + $this->watcher->detach($this->getIdentity()); foreach ($this->connections as $connection) { $connection->disconnect(); $this->dispatch('disconnect', [$this, $connection]); @@ -472,7 +510,9 @@ public function disconnect(): void $this->server->close(); } $this->server = null; - $this->configuration->getLogger()->info('[server] Server disconnected'); + $this->configuration->getLogger()->info('[{identity}] Server disconnected', [ + 'identity' => $this->identity, + ]); } @@ -485,18 +525,30 @@ protected function createSocketServer(): void $uri = new Uri("{$this->scheme}://0.0.0.0:{$this->port}"); $this->server = $this->streamFactory->createSocketServer($uri, $this->configuration->getContext()); /** @throws StreamException */ - $this->watcher->attach('@server', $this->server, function (string $key, SocketServer $socket) { + $this->watcher->attach($this->getIdentity(), $this->server, function (string $key, SocketServer $socket) { $this->acceptHandler($socket); }); $this->allowConnections = true; $this->configuration->getLogger()->info("[server] Starting server on {$uri}."); + $this->configuration->getLogger()->info('[{identity}] Starting server on {uri}', [ + 'identity' => $this->identity, + 'uri' => $uri, + ]); } catch (StreamException $e) { $error = "Server failed to start: {$e->getMessage()}"; - $this->configuration->getLogger()->error("[server] {$error}"); + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, + 'exception' => $e, + 'error' => $error, + ]); throw new ServerException($this, $error, $e); } catch (Throwable $e) { $error = "Server error: {$e->getMessage()}"; - $this->configuration->getLogger()->error("[server] {$error}"); + $this->configuration->getLogger()->error('[{identity}] {error}', [ + 'identity' => $this->identity, + 'exception' => $e, + 'error' => $error, + ]); throw $e; } } @@ -506,31 +558,34 @@ protected function acceptHandler(SocketServer $socket): void { $maxConnections = $this->configuration->getMaxConnections(); if (!is_null($maxConnections) && $this->getConnectionCount() >= $maxConnections) { - $this->configuration->getLogger()->warning( - "[server] Denied connection, reached max {$maxConnections}" - ); + $this->configuration->getLogger()->warning('[{identity}] Denied connection, reached max {maxConnections}', [ + 'identity' => $this->identity, + 'maxConnections' => $maxConnections, + ]); return; } if (!$this->allowConnections) { $this->configuration->getLogger()->warning("[server] Denied connection, shutting down"); + $this->configuration->getLogger()->warning('[{identity}] ', [ + 'identity' => $this->identity, + ]); return; } try { /** @var SocketStream $stream */ $stream = $socket->accept(); - $name = $stream->getRemoteName() ?? 'unknown'; - $this->watcher->attach($name, $stream, function ($key, $stream) { - $this->selectHandler($key, $stream); - }); $connection = new Connection( + $this, $stream, false, true, $this->isSsl(), $this->httpFactory, $this->configuration, - $this ); + $this->watcher->attach($connection->getIdentity(), $stream, function ($key, $stream) { + $this->selectHandler($key, $stream); + }); } catch (StreamException $e) { throw new ConnectionFailureException(null, "Server failed to accept: {$e->getMessage()}", $e); } @@ -540,8 +595,11 @@ protected function acceptHandler(SocketServer $socket): void } /** @throws StreamException */ $request = $this->performHandshake($connection); - $this->connections[$name] = $connection; - $this->configuration->getLogger()->info("[server] Accepted connection from {$name}."); + $this->connections[$connection->getIdentity()] = $connection; + $this->configuration->getLogger()->info('[{identity}] Accepted connection from {connection}', [ + 'identity' => $this->identity, + 'connection' => $connection->getIdentity(), + ]); $this->dispatch('handshake', [ $this, $connection, @@ -562,7 +620,10 @@ protected function detachUnconnected(): void if (!$connection->isConnected()) { $this->watcher->detach($key); unset($this->connections[$key]); - $this->configuration->getLogger()->info("[server] Disconnected {$key}."); + $this->configuration->getLogger()->info('[{identity}] Disconnected {key}.', [ + 'identity' => $this->identity, + 'connection' => $connection->getIdentity(), + ]); $this->dispatch('disconnect', [$this, $connection]); } } @@ -633,7 +694,11 @@ protected function performHandshake(Connection $connection): ServerRequest ->withHeader('Connection', 'Upgrade') ->withHeader('Sec-WebSocket-Accept', $responseKey); } catch (HandshakeException $e) { - $this->configuration->getLogger()->warning("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->warning('[{identity}] {error} (handshake)', [ + 'identity' => $this->identity, + 'exception' => $e, + 'error' => $e->getMessage(), + ]); $response = $e->getResponse(); $exception = $e; } @@ -653,7 +718,11 @@ protected function performHandshake(Connection $connection): ServerRequest throw $exception; } - $this->configuration->getLogger()->debug("[server] Handshake on {$request->getUri()->getPath()}"); + $this->configuration->getLogger()->debug('[{identity}] Handshake on {path}', [ + 'identity' => $this->identity, + 'path' => $request->getUri()->getPath(), + ]); + $connection->setHandshakeRequest($request); $connection->setHandshakeResponse($response); diff --git a/src/Trait/StringableTrait.php b/src/Trait/StringableTrait.php index ba67f1d..8cba7c8 100644 --- a/src/Trait/StringableTrait.php +++ b/src/Trait/StringableTrait.php @@ -18,7 +18,7 @@ public function __toString(): string return get_class($this); } - protected function stringable(string $format, mixed ...$values): string + protected function stringable(string $format = '', mixed ...$values): string { return sprintf("%s({$format})", get_class($this), ...$values); } diff --git a/tests/mock/MockStreamTrait.php b/tests/mock/MockStreamTrait.php index 810fe28..f454721 100644 --- a/tests/mock/MockStreamTrait.php +++ b/tests/mock/MockStreamTrait.php @@ -94,20 +94,16 @@ private function expectWsClientConnect( $this->expectContext()->addAssert(function ($method, $params) { $this->assertIsResource($params[0]); }); - $this->expectSocketStreamGetRemoteName()->setReturn(function () use ($host, $port) { - return "{$host}:{$port}"; - }); - $this->expectStreamCollectionAttach(); $this->expectSocketStreamGetLocalName()->setReturn(function () use ($local) { return "{$local}"; }); $this->expectSocketStreamGetRemoteName()->setReturn(function () use ($host, $port) { return "{$host}:{$port}"; }); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($timeout) { $this->assertEquals($timeout, $params[0]); }); + $this->expectStreamCollectionAttach(); $this->expectSocketStreamIsConnected(); if ($persistent) { $this->expectSocketStreamTell(); @@ -189,8 +185,8 @@ private function expectWsServerSetup(string $scheme = 'tcp', int $port = 8000, a } $this->expectSocketServerGetMetadata(); - $this->expectStreamCollectionAttach()->addAssert(function ($method, $params) { - $this->assertEquals('@server', $params[1]); + $this->expectStreamCollectionAttach()->addAssert(function ($method, $params) use ($port) { + $this->assertEquals("server/{$port}", $params[1]); }); } @@ -201,10 +197,9 @@ private function expectWsSelectConnections(array $keys = []): StackItem { $this->expectStreamCollectionCount(); $this->expectStreamCollectionWaitRead()->setReturn(function ($params, $default, $collection) use ($keys) { - $keys = array_flip($keys); $selected = new StreamCollection(); foreach ($collection as $key => $stream) { - if (array_key_exists($key, $keys)) { + if (in_array($key, $keys)) { $selected->attach($stream, $key); } } @@ -223,10 +218,9 @@ private function expectWsSelectConnections(array $keys = []): StackItem private function expectWsClientSelectConnections(array $keys = []): StackItem { $this->expectStreamCollectionWaitRead()->setReturn(function ($params, $default, $collection) use ($keys) { - $keys = array_flip($keys); $selected = new StreamCollection(); foreach ($collection as $key => $stream) { - if (array_key_exists($key, $keys)) { + if (in_array($key, $keys)) { $selected->attach($stream, $key); } } @@ -239,6 +233,24 @@ private function expectWsClientSelectConnections(array $keys = []): StackItem return $last; } + private function expectWsServerAccept( + string $local = 'localhost:8000', + string $remote = '127.0.0.1:12345' + ): StackItem { + $this->expectSocketServerAccept(); + $this->expectSocketStream(); + $this->expectSocketStreamGetMetadata(); + $this->expectContext(); + $this->expectSocketStreamGetLocalName()->setReturn(function () use ($local) { + return $local; + }); + $this->expectSocketStreamGetRemoteName()->setReturn(function () use ($remote) { + return $remote; + }); + $this->expectSocketStreamSetTimeout(); + return $this->expectStreamCollectionAttach(); + } + /** * @param array $headers */ @@ -246,7 +258,7 @@ private function expectWsServerPerformHandshake( string $host = 'localhost:8000', string $path = '/my/mock/path', array $headers = [] - ): void { + ): StackItem { $this->expectSocketStreamReadLine()->addAssert(function (string $method, array $params): void { $this->assertEquals(1024, $params[0]); })->setReturn(function (array $params) use ($path) { @@ -294,7 +306,7 @@ private function expectWsServerPerformHandshake( })->setReturn(function (array $params) { return "\r\n"; }); - $this->expectSocketStreamWrite()->addAssert(function (string $method, array $params): void { + return $this->expectSocketStreamWrite()->addAssert(function (string $method, array $params): void { $expect = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n" . "Sec-WebSocket-Accept: YmysboNHNoWzWVeQpduY7xELjgU=\r\n\r\n"; $this->assertEquals($expect, $params[0]); diff --git a/tests/suites/client/ClientErrorTest.php b/tests/suites/client/ClientErrorTest.php index 6908016..dfd0c43 100644 --- a/tests/suites/client/ClientErrorTest.php +++ b/tests/suites/client/ClientErrorTest.php @@ -80,11 +80,14 @@ public function testFailedConnection(): void $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); $this->expectContext(); - $this->expectSocketStreamGetRemoteName(); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName(); - $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamGetLocalName()->setReturn(function () { + return "12:34"; + }); + $this->expectSocketStreamGetRemoteName()->setReturn(function () { + return "56:78"; + }); $this->expectSocketStreamSetTimeout(); + $this->expectStreamCollectionAttach(); $this->expectSocketStreamIsConnected()->setReturn(function () { return false; }); diff --git a/tests/suites/client/ClientTest.php b/tests/suites/client/ClientTest.php index 46d2d93..9028863 100644 --- a/tests/suites/client/ClientTest.php +++ b/tests/suites/client/ClientTest.php @@ -300,7 +300,7 @@ public function testPayload65536(): void unset($client); } - public function stestMultiFrame(): void + public function testMultiFrame(): void { $this->expectWsClientCreate(); $client = new Client('ws://localhost:8000/my/mock/path', streamFactory: new StreamFactory()); @@ -888,7 +888,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -898,7 +898,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -908,7 +908,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -918,7 +918,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -928,7 +928,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -938,7 +938,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -948,7 +948,7 @@ public function testListeners(): void $client->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -1000,7 +1000,7 @@ public function testRunConnectionClosedException(): void $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -1029,7 +1029,7 @@ public function testRunClientException(): void $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () use ($client) { @@ -1050,7 +1050,7 @@ public function testRunExternalException(): void $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectWsClientSelectConnections(['localhost:8000'])->setReturn(function () { + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000'])->setReturn(function () { throw new StreamException(1000); }); $this->expectStreamCollectionDetach(); @@ -1075,7 +1075,7 @@ public function testCloseException(): void $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('gQA='); }); @@ -1083,7 +1083,7 @@ public function testCloseException(): void $this->expectSocketStreamIsReadable(); $this->expectSocketStreamCloseWrite(); $this->expectSocketStreamGetMetadata(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('iIk='); }); @@ -1115,7 +1115,7 @@ public function testReconnectException(): void }); $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('gQA='); // text }); @@ -1124,7 +1124,7 @@ public function testReconnectException(): void $this->expectSocketStreamCloseWrite(); $this->expectSocketStreamGetMetadata(); $this->expectSocketStreamIsConnected(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('iBo='); // close }); @@ -1135,7 +1135,7 @@ public function testReconnectException(): void $this->expectSocketStreamClose(); $this->expectSocketStreamIsConnected(); $this->expectSocketStreamIsConnected(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('ggA='); // binary }); @@ -1166,7 +1166,7 @@ public function testUnresolvableError(): void $this->expectWsClientConnect(); $this->expectWsClientPerformHandshake(); - $this->expectWsClientSelectConnections(['localhost:8000']); + $this->expectWsClientSelectConnections(['client/localhost/connection/12345/8000']); $this->expectSocketStreamRead()->setReturn(function () { return base64_decode('gQA='); }); diff --git a/tests/suites/connection/ConnectionTest.php b/tests/suites/connection/ConnectionTest.php index 8ae6200..29e7495 100644 --- a/tests/suites/connection/ConnectionTest.php +++ b/tests/suites/connection/ConnectionTest.php @@ -18,7 +18,10 @@ }; use Psr\Log\NullLogger; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Client, + Connection, +}; use WebSocket\Exception\{ BadOpcodeException, BadUriException, @@ -60,6 +63,7 @@ public function tearDown(): void public function testCreate(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -67,10 +71,10 @@ public function testCreate(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->assertInstanceOf(Connection::class, $connection); $this->assertInstanceOf(Stringable::class, $connection); - $this->assertNull($connection->getHandler()); + $this->assertInstanceOf(Client::class, $connection->getHandler()); $this->expectSocketStreamIsConnected(); $this->assertTrue($connection->isConnected()); @@ -115,6 +119,7 @@ public function testCreate(): void public function testHttpMessages(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -122,7 +127,7 @@ public function testHttpMessages(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $request = new Request('GET', 'ws://test.com/path'); $connection->setHandshakeRequest($request); $this->assertSame($request, $connection->getHandshakeRequest()); @@ -156,6 +161,7 @@ public function testHttpMessages(): void public function testWebSocketMessages(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -163,7 +169,7 @@ public function testWebSocketMessages(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $message = new Text('Test message'); $this->expectSocketStreamWrite()->addAssert(function ($method, $params) { @@ -190,6 +196,7 @@ public function testWebSocketMessages(): void public function testSendHttpError(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -197,7 +204,7 @@ public function testSendHttpError(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () use ($connection) { throw new ConnectionClosedException($connection); @@ -212,6 +219,7 @@ public function testSendHttpError(): void public function testPullHttpError(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -219,7 +227,7 @@ public function testPullHttpError(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamReadLine()->setReturn(function () use ($connection) { throw new ConnectionClosedException($connection); @@ -234,6 +242,7 @@ public function testPullHttpError(): void public function testSendMessageError(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -241,7 +250,7 @@ public function testSendMessageError(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () use ($connection) { throw new ConnectionClosedException($connection); @@ -256,6 +265,7 @@ public function testSendMessageError(): void public function testPullMessageError(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -263,7 +273,7 @@ public function testPullMessageError(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamRead()->setReturn(function () use ($connection) { throw new ConnectionClosedException($connection); diff --git a/tests/suites/connection/ExceptionTest.php b/tests/suites/connection/ExceptionTest.php index d45b400..f594326 100644 --- a/tests/suites/connection/ExceptionTest.php +++ b/tests/suites/connection/ExceptionTest.php @@ -17,7 +17,10 @@ ExpectSocketStreamTrait, }; use RuntimeException; -use WebSocket\Connection; +use WebSocket\{ + Client, + Connection, +}; use WebSocket\Exception\{ BadOpcodeException, BadUriException, @@ -51,13 +54,14 @@ public function tearDown(): void public function testBadOpcodeException(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); $this->expectContext(); $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { throw new BadOpcodeException(); @@ -73,13 +77,14 @@ public function testBadOpcodeException(): void public function testBadUriException(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); $this->expectContext(); $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { throw new BadUriException(); @@ -94,13 +99,14 @@ public function testBadUriException(): void public function testConnectionClosedException(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); $this->expectContext(); $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () use ($connection) { throw new ConnectionClosedException($connection); @@ -115,13 +121,14 @@ public function testConnectionClosedException(): void public function testConnectionFailureException(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); $this->expectContext(); $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () use ($connection) { throw new ConnectionFailureException($connection); @@ -136,13 +143,14 @@ public function testConnectionFailureException(): void public function testConnectionTimeoutException(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); $this->expectContext(); $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { throw new ConnectionTimeoutException(); @@ -157,13 +165,14 @@ public function testConnectionTimeoutException(): void public function testGenericTimeoutException(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); $this->expectContext(); $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { throw new RuntimeException('Generic error', 77); @@ -182,13 +191,14 @@ public function testGenericTimeoutException(): void public function testGenericEofException(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); $this->expectContext(); $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { throw new RuntimeException('Generic error', 77); @@ -207,13 +217,14 @@ public function testGenericEofException(): void public function testGenericUnconnectedException(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); $this->expectContext(); $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { throw new RuntimeException('Generic error', 77); @@ -231,13 +242,14 @@ public function testGenericUnconnectedException(): void public function testGenericConnectedException(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); $this->expectContext(); $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { throw new RuntimeException('Generic error', 77); diff --git a/tests/suites/middleware/CallbackTest.php b/tests/suites/middleware/CallbackTest.php index c5b2c46..ab726aa 100644 --- a/tests/suites/middleware/CallbackTest.php +++ b/tests/suites/middleware/CallbackTest.php @@ -13,7 +13,10 @@ use Phrity\Net\Mock\SocketStream; use Psr\Log\NullLogger; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Client, + Connection, +}; use WebSocket\Http\{ Request, Response, @@ -43,6 +46,7 @@ public function tearDown(): void public function testIncoming(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -50,7 +54,7 @@ public function testIncoming(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $middleware = new Callback(incoming: function ($stack, $connection) { $message = $stack->handleIncoming(); @@ -78,6 +82,7 @@ public function testIncoming(): void public function testOutgoing(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -85,7 +90,7 @@ public function testOutgoing(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware(new Callback(outgoing: function ($stack, $connection, $message) { $this->assertEquals('Test message', $message->getContent()); @@ -104,6 +109,7 @@ public function testOutgoing(): void public function testHttpIncoming(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -111,7 +117,7 @@ public function testHttpIncoming(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware(new Callback(httpIncoming: function ($stack, $connection) { $message = $stack->handleHttpIncoming(); @@ -138,6 +144,7 @@ public function testHttpIncoming(): void public function testHttpOutgoing(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -145,7 +152,7 @@ public function testHttpOutgoing(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware(new Callback(httpOutgoing: function ($stack, $connection, $message) { $message = $stack->handleHttpOutgoing($message); @@ -164,6 +171,7 @@ public function testHttpOutgoing(): void public function testTick(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -171,7 +179,7 @@ public function testTick(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware(new Callback(tick: function ($stack, $connection) { $stack->handleTick(); diff --git a/tests/suites/middleware/CloseHandlerTest.php b/tests/suites/middleware/CloseHandlerTest.php index 867bc09..8db2ab4 100644 --- a/tests/suites/middleware/CloseHandlerTest.php +++ b/tests/suites/middleware/CloseHandlerTest.php @@ -12,7 +12,10 @@ use PHPUnit\Framework\TestCase; use Phrity\Net\Mock\SocketStream; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Client, + Connection, +}; use WebSocket\Message\Close; use WebSocket\Middleware\CloseHandler; use WebSocket\Test\MockStreamTrait; @@ -38,6 +41,7 @@ public function tearDown(): void public function testLocalClose(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -45,7 +49,7 @@ public function testLocalClose(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $middleware = new CloseHandler(); $connection->addMiddleware($middleware); @@ -72,6 +76,7 @@ public function testLocalClose(): void public function testRemoteClose(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -79,7 +84,7 @@ public function testRemoteClose(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware(new CloseHandler()); $this->expectWsReadMessage('iAY==', 'A+h0dGZu'); diff --git a/tests/suites/middleware/DeflateCompressorTest.php b/tests/suites/middleware/DeflateCompressorTest.php index 38b8433..a34a1c6 100644 --- a/tests/suites/middleware/DeflateCompressorTest.php +++ b/tests/suites/middleware/DeflateCompressorTest.php @@ -14,7 +14,10 @@ use RangeException; use RuntimeException; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Client, + Connection, +}; use WebSocket\Http\{ Request, Response, @@ -45,6 +48,7 @@ public function tearDown(): void public function testClientDefault(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -52,7 +56,7 @@ public function testClientDefault(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $compressor = new DeflateCompressor(); $this->assertInstanceOf(Stringable::class, $compressor); @@ -138,6 +142,7 @@ function (string $method, array $params): void { public function testServerDefault(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -145,7 +150,7 @@ public function testServerDefault(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $compressor = new DeflateCompressor(); $this->assertInstanceOf(Stringable::class, $compressor); @@ -231,6 +236,7 @@ function (string $method, array $params): void { public function testClientServerDeclines(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -238,7 +244,7 @@ public function testClientServerDeclines(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $compressor = new DeflateCompressor(); $this->assertInstanceOf(Stringable::class, $compressor); @@ -303,6 +309,7 @@ function (string $method, array $params): void { public function testClientConfiguration(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -310,7 +317,7 @@ public function testClientConfiguration(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $compressor = new DeflateCompressor( clientNoContextTakeover: true, @@ -401,6 +408,7 @@ function (string $method, array $params): void { public function testServerConfiguration(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -408,7 +416,7 @@ public function testServerConfiguration(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $compressor = new DeflateCompressor( clientNoContextTakeover: true, @@ -498,6 +506,7 @@ function (string $method, array $params): void { public function testClientConfigurationByServer(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -505,7 +514,7 @@ public function testClientConfigurationByServer(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $compressor = new DeflateCompressor( clientMaxWindowBits: 10, @@ -562,6 +571,7 @@ function (string $method, array $params): void { public function testServerConfigurationByServer(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -569,7 +579,7 @@ public function testServerConfigurationByServer(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $compressor = new DeflateCompressor( clientNoContextTakeover: true, diff --git a/tests/suites/middleware/FollowRedirectTest.php b/tests/suites/middleware/FollowRedirectTest.php index b54cb65..9cd3fb1 100644 --- a/tests/suites/middleware/FollowRedirectTest.php +++ b/tests/suites/middleware/FollowRedirectTest.php @@ -17,7 +17,7 @@ use Stringable; use WebSocket\{ Client, - Connection + Connection, }; use WebSocket\Exception\{ HandshakeException, @@ -47,6 +47,7 @@ public function tearDown(): void public function testRedirect(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $middleware = new FollowRedirect(2); $this->assertEquals('WebSocket\Middleware\FollowRedirect', "{$middleware}"); @@ -58,7 +59,7 @@ public function testRedirect(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware($middleware); $this->expectSocketStreamReadLine()->setReturn(function () { @@ -78,6 +79,7 @@ public function testRedirect(): void public function testMaxRedirect(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $middleware = new FollowRedirect(0); @@ -87,7 +89,7 @@ public function testMaxRedirect(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware($middleware); $this->expectSocketStreamReadLine()->setReturn(function () { @@ -107,6 +109,7 @@ public function testMaxRedirect(): void public function testNoLocation(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $middleware = new FollowRedirect(0); @@ -116,7 +119,7 @@ public function testNoLocation(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware($middleware); $this->expectSocketStreamReadLine()->setReturn(function () { diff --git a/tests/suites/middleware/PingIntervalTest.php b/tests/suites/middleware/PingIntervalTest.php index f20d73b..d4b1045 100644 --- a/tests/suites/middleware/PingIntervalTest.php +++ b/tests/suites/middleware/PingIntervalTest.php @@ -12,7 +12,10 @@ use PHPUnit\Framework\TestCase; use Phrity\Net\Mock\SocketStream; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Client, + Connection, +}; use WebSocket\Middleware\PingInterval; use WebSocket\Test\MockStreamTrait; @@ -37,6 +40,7 @@ public function tearDown(): void public function testPingInterval(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $middleware = new PingInterval(1); $this->assertEquals('WebSocket\Middleware\PingInterval', "{$middleware}"); @@ -48,7 +52,7 @@ public function testPingInterval(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware($middleware); // First tick set interval diff --git a/tests/suites/middleware/PingResponderTest.php b/tests/suites/middleware/PingResponderTest.php index 7e350f1..91a42fa 100644 --- a/tests/suites/middleware/PingResponderTest.php +++ b/tests/suites/middleware/PingResponderTest.php @@ -12,7 +12,10 @@ use PHPUnit\Framework\TestCase; use Phrity\Net\Mock\SocketStream; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Client, + Connection, +}; use WebSocket\Message\Ping; use WebSocket\Middleware\PingResponder; use WebSocket\Test\MockStreamTrait; @@ -38,6 +41,7 @@ public function tearDown(): void public function testPingAutoResponse(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -45,7 +49,7 @@ public function testPingAutoResponse(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $middleware = new PingResponder(); $connection->addMiddleware($middleware); diff --git a/tests/suites/middleware/ProcessStackTest.php b/tests/suites/middleware/ProcessStackTest.php index 1633d5c..a487043 100644 --- a/tests/suites/middleware/ProcessStackTest.php +++ b/tests/suites/middleware/ProcessStackTest.php @@ -12,7 +12,10 @@ use PHPUnit\Framework\TestCase; use Phrity\Net\Mock\SocketStream; use Psr\Log\NullLogger; -use WebSocket\Connection; +use WebSocket\{ + Client, + Connection, +}; use WebSocket\Message\Text; use WebSocket\Middleware\{ Callback, @@ -42,6 +45,7 @@ public function tearDown(): void public function testIncoming(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -49,7 +53,7 @@ public function testIncoming(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware(new Callback(incoming: function ($stack, $connection) { $message = $stack->handleIncoming(); @@ -88,6 +92,7 @@ public function testIncoming(): void public function testOutgoing(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $this->expectSocketStream(); $this->expectSocketStreamGetMetadata(); @@ -95,7 +100,7 @@ public function testOutgoing(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware(new Callback(outgoing: function ($stack, $connection, $message) { $this->assertEquals('Test message', $message->getContent()); diff --git a/tests/suites/middleware/SubprotocolNegotiationTest.php b/tests/suites/middleware/SubprotocolNegotiationTest.php index f7d9392..dfd6620 100644 --- a/tests/suites/middleware/SubprotocolNegotiationTest.php +++ b/tests/suites/middleware/SubprotocolNegotiationTest.php @@ -16,7 +16,10 @@ Request, Response, }; -use WebSocket\Connection; +use WebSocket\{ + Client, + Connection, +}; use WebSocket\Exception\HandshakeException; use WebSocket\Middleware\SubprotocolNegotiation; use WebSocket\Test\MockStreamTrait; @@ -42,6 +45,7 @@ public function tearDown(): void public function testClientProtocolMatch(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $middleware = new SubprotocolNegotiation(['sp-1', 'sp-2', 'sp-3']); $this->assertEquals('WebSocket\Middleware\SubprotocolNegotiation', "{$middleware}"); @@ -53,7 +57,7 @@ public function testClientProtocolMatch(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware($middleware); $this->expectSocketStreamWrite()->addAssert( @@ -91,6 +95,7 @@ function (string $method, array $params): void { public function testClientProtocolNoMatch(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $middleware = new SubprotocolNegotiation(['sp-1', 'sp-2', 'sp-3']); $this->assertEquals('WebSocket\Middleware\SubprotocolNegotiation', "{$middleware}"); @@ -102,7 +107,7 @@ public function testClientProtocolNoMatch(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware($middleware); $this->expectSocketStreamWrite()->addAssert( @@ -137,6 +142,7 @@ function (string $method, array $params): void { public function testClientProtocolRequire(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $middleware = new SubprotocolNegotiation(['sp-1', 'sp-2', 'sp-3'], true); $this->assertEquals('WebSocket\Middleware\SubprotocolNegotiation', "{$middleware}"); @@ -148,7 +154,7 @@ public function testClientProtocolRequire(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware($middleware); $this->expectSocketStreamWrite()->addAssert( @@ -182,6 +188,7 @@ function (string $method, array $params): void { public function testServerProtocolMatch(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $middleware = new SubprotocolNegotiation(['sp-1', 'sp-2', 'sp-3']); $this->assertEquals('WebSocket\Middleware\SubprotocolNegotiation', "{$middleware}"); @@ -193,7 +200,7 @@ public function testServerProtocolMatch(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware($middleware); $this->expectSocketStreamReadLine()->setReturn(function () { @@ -237,6 +244,7 @@ function (string $method, array $params): void { public function testServerProtocolNoMatch(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $middleware = new SubprotocolNegotiation(['sp-1', 'sp-2', 'sp-3']); $this->assertEquals('WebSocket\Middleware\SubprotocolNegotiation', "{$middleware}"); @@ -248,7 +256,7 @@ public function testServerProtocolNoMatch(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware($middleware); $this->expectSocketStreamReadLine()->setReturn(function () { @@ -293,6 +301,7 @@ function (string $method, array $params): void { public function testServerProtocolRequire(): void { $temp = tmpfile(); + $client = new Client('ws://localhost:8000/my/mock/path'); $middleware = new SubprotocolNegotiation(['sp-1', 'sp-2', 'sp-3'], true); $this->assertEquals('WebSocket\Middleware\SubprotocolNegotiation', "{$middleware}"); @@ -304,7 +313,7 @@ public function testServerProtocolRequire(): void $stream = new SocketStream($temp); $this->expectWsConnectionCreate(); - $connection = new Connection($stream, false, false); + $connection = new Connection($client, $stream, false, false); $connection->addMiddleware($middleware); $this->expectSocketStreamReadLine()->setReturn(function () { diff --git a/tests/suites/server/ConfigTest.php b/tests/suites/server/ConfigTest.php index df9eef5..a3a0e38 100644 --- a/tests/suites/server/ConfigTest.php +++ b/tests/suites/server/ConfigTest.php @@ -103,21 +103,8 @@ public function testServerConfiguration(): void $this->assertSame($server, $server->setStreamFactory(new StreamFactory())); $this->expectWsServerSetup(scheme: 'ssl', port: 9000); - $this->expectWsSelectConnections(['@server']); - // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/9000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); $this->expectWsServerPerformHandshake(); @@ -175,21 +162,8 @@ public function testContext(): void $this->assertSame($context, $server->getContext()); $this->expectWsServerSetup(context: ['ssl' => ['verify_peer' => false]]); - $this->expectWsSelectConnections(['@server']); - // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept(); $this->expectWsServerPerformHandshake(); $server->start(); diff --git a/tests/suites/server/HandshakeTest.php b/tests/suites/server/HandshakeTest.php index 0b40424..c2672dc 100644 --- a/tests/suites/server/HandshakeTest.php +++ b/tests/suites/server/HandshakeTest.php @@ -60,18 +60,8 @@ public function testHandshakeRequest(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return "fake-connection"; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName(); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); $this->expectWsServerPerformHandshake(); @@ -91,18 +81,8 @@ public function testHandshakeRequestVariant(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return "fake-connection"; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName(); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); @@ -150,18 +130,8 @@ public function testHandshakeRequestFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return "fake-connection"; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName(); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); $this->expectSocketStreamReadLine()->setReturn(function () { @@ -185,18 +155,8 @@ public function testHandshakeMethodFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return "fake-connection"; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName(); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); $this->expectSocketStreamReadLine()->setReturn(function () { @@ -239,18 +199,8 @@ public function testHandshakeConnectionHeaderFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return "fake-connection"; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName(); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); $this->expectSocketStreamReadLine()->setReturn(function () { @@ -293,18 +243,8 @@ public function testHandshakeUpgradeHeaderFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return "fake-connection"; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName(); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); $this->expectSocketStreamReadLine()->setReturn(function () { @@ -347,18 +287,8 @@ public function testHandshakeVersionHeaderFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return "fake-connection"; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName(); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); $this->expectSocketStreamReadLine()->setReturn(function () { @@ -401,18 +331,8 @@ public function testHandshakeWebSocketKeyHeaderFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return "fake-connection"; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName(); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); $this->expectSocketStreamReadLine()->setReturn(function () { @@ -452,18 +372,8 @@ public function testHandshakeWebSocketKeyInvalidFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return "fake-connection"; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName(); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); $this->expectSocketStreamReadLine()->setReturn(function () { @@ -506,18 +416,8 @@ public function testHandshakeResponseFailure(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return "fake-connection"; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName(); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); $this->expectSocketStreamReadLine()->setReturn(function () { diff --git a/tests/suites/server/ServerTest.php b/tests/suites/server/ServerTest.php index 287c0db..c0c5adb 100644 --- a/tests/suites/server/ServerTest.php +++ b/tests/suites/server/ServerTest.php @@ -140,26 +140,13 @@ public function testListeners(): void }); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept(); $this->expectWsServerPerformHandshake(); $server->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['fake-connection-1']); + $this->expectWsSelectConnections(['server/8000/connection/8000/12345']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -173,7 +160,7 @@ public function testListeners(): void $server->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['fake-connection-1']); + $this->expectWsSelectConnections(['server/8000/connection/8000/12345']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -187,7 +174,7 @@ public function testListeners(): void $server->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['fake-connection-1']); + $this->expectWsSelectConnections(['server/8000/connection/8000/12345']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -201,7 +188,7 @@ public function testListeners(): void $server->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['fake-connection-1']); + $this->expectWsSelectConnections(['server/8000/connection/8000/12345']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -215,7 +202,7 @@ public function testListeners(): void $server->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['fake-connection-1']); + $this->expectWsSelectConnections(['server/8000/connection/8000/12345']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -229,7 +216,7 @@ public function testListeners(): void $server->start(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['fake-connection-1']); + $this->expectWsSelectConnections(['server/8000/connection/8000/12345']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -262,21 +249,8 @@ public function testMiddlewares(): void $server->addMiddleware(new Callback()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); $this->expectWsServerPerformHandshake(); @@ -298,21 +272,8 @@ public function testBroadcastSend(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($server) { + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); }); $this->expectWsServerPerformHandshake(); @@ -362,42 +323,16 @@ public function testDetachConnection(): void }); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept(); $this->expectWsServerPerformHandshake(); $this->expectSocketStreamClose(); $server->start(); $this->expectSocketStreamIsConnected(); $this->expectStreamCollectionDetach(); - $this->expectWsSelectConnections(['@server']); - // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept(); $this->expectWsServerPerformHandshake(); $this->expectSocketStreamClose(); $server->start(); @@ -417,7 +352,7 @@ public function testShutdown(): void $server->addMiddleware(new CloseHandler()); $server->onHandshake(function ($server, $connection, $request, $response) { - if ($connection->getName() == 'fake-connection-2') { + if ($connection->getIdentity() == 'server/8000/connection/8000/12346') { $server->shutdown(); } }); @@ -425,38 +360,14 @@ public function testShutdown(): void $this->expectWsServerSetup(scheme: 'tcp', port: 8000); // Accept connection 1 - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept(remote: '127.0.0.1:12345'); $this->expectWsServerPerformHandshake(); $this->expectSocketStreamIsConnected(); // Accept connection 2 - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-2'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-2'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept(remote: '127.0.0.1:12346'); $this->expectWsServerPerformHandshake(); // Send close connection 1 @@ -475,8 +386,12 @@ public function testShutdown(): void $this->expectSocketStreamIsConnected(); $this->expectSocketStreamIsConnected(); - // The @server handler should be blocked now - $this->expectWsSelectConnections(['@server', 'fake-connection-1', 'fake-connection-2']); + // The server handler should be blocked now + $this->expectWsSelectConnections([ + 'server/8000', + 'server/8000/connection/8000/12345', + 'server/8000/connection/8000/12346', + ]); // Receive close ack connection 1 $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { @@ -555,21 +470,9 @@ public function testAlreadyStarted(): void }); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server/8000']); // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsServerAccept(); $this->expectWsServerPerformHandshake(); $this->expectSocketStreamClose(); $server->start(); @@ -626,7 +529,7 @@ public function testServerAccessError(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server/8000']); $this->expectSocketServerAccept()->addAssert(function ($method, $params) use ($server) { $server->stop(); throw new StreamException(StreamException::SERVER_ACCEPT_ERR, ['uri' => 'test']); @@ -646,24 +549,11 @@ public function testRunBadOpcodeException(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept(); $this->expectWsServerPerformHandshake(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['fake-connection-1']); + $this->expectWsSelectConnections(['server/8000/connection/8000/12345']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () use ($server) { @@ -694,24 +584,11 @@ public function testRunConnectionClosedException(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept(); $this->expectWsServerPerformHandshake(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['fake-connection-1']); + $this->expectWsSelectConnections(['server/8000/connection/8000/12345']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () use ($server) { @@ -741,24 +618,11 @@ public function testRunServerException(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept(); $this->expectWsServerPerformHandshake(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['fake-connection-1']); + $this->expectWsSelectConnections(['server/8000/connection/8000/12345']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () use ($server) { @@ -789,24 +653,11 @@ public function testRunExternalException(): void $server = new Server(8000, streamFactory: new StreamFactory()); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept(); $this->expectWsServerPerformHandshake(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['fake-connection-1'])->setReturn(function () use ($server) { + $this->expectWsSelectConnections(['server/8000/connection/8000/12345'])->setReturn(function () use ($server) { $server->stop(); throw new StreamException(1000); }); @@ -834,25 +685,12 @@ public function testUnmaskedException(): void }); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept(); $this->expectWsServerPerformHandshake(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['fake-connection-1']); + $this->expectWsSelectConnections(['server/8000/connection/8000/12345']); $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) { $this->assertEquals(2, $params[0]); })->setReturn(function () { @@ -876,23 +714,11 @@ public function testMaxConnectionsOverflow(): void $server->setMaxConnections(1); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsSelectConnections(['server/8000']); + $this->expectWsServerAccept(); $this->expectWsServerPerformHandshake(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['@server'])->addAssert(function () use ($server) { + $this->expectWsSelectConnections(['server/8000'])->addAssert(function () use ($server) { $server->stop(); }); @@ -921,21 +747,9 @@ public function testUnresolvableError(): void }); $this->expectWsServerSetup(scheme: 'tcp', port: 8000); - $this->expectWsSelectConnections(['@server']); + $this->expectWsSelectConnections(['server/8000']); // Accept connection - $this->expectSocketServerAccept(); - $this->expectSocketStream(); - $this->expectSocketStreamGetMetadata(); - $this->expectContext(); - $this->expectSocketStreamGetRemoteName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectStreamCollectionAttach(); - $this->expectSocketStreamGetLocalName()->setReturn(function () { - return 'fake-connection-1'; - }); - $this->expectSocketStreamGetRemoteName(); - $this->expectSocketStreamSetTimeout(); + $this->expectWsServerAccept(); $this->expectWsServerPerformHandshake(); $this->expectStreamCollectionDetach(); $this->expectSocketStreamClose(); From bbb0b7dbaf59d91ca02f9bc6ca5af8acb424091c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Sat, 18 Apr 2026 13:46:08 +0200 Subject: [PATCH 16/16] rm handled --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/workflows/acceptance.yml | 18 +- README.md | 5 +- composer.json | 20 +-- docs/Changelog.md | 19 ++- docs/Class_Synopsis.md | 163 ++++++++----------- docs/Client.md | 14 +- docs/Configuration.md | 8 +- docs/Connection.md | 37 ++++- docs/Contributing.md | 2 +- docs/Examples.md | 27 ++- docs/Index.md | 3 +- docs/Migrate_1_2.md | 56 +++++++ docs/Server.md | 15 +- docu.json | 14 +- examples/delegating_server.php | 47 +++--- examples/echoserver.php | 26 +-- examples/random_client.php | 33 ++-- examples/random_server.php | 26 +-- examples/send.php | 33 ++-- src/Configuration.php | 2 +- src/Constant.php | 6 +- src/Exception/BadOpcodeException.php | 9 +- src/Exception/BadUriException.php | 9 +- src/Exception/ClientException.php | 4 +- src/Exception/CloseException.php | 11 +- src/Exception/ConnectionClosedException.php | 9 +- src/Exception/ConnectionFailureException.php | 9 +- src/Exception/ConnectionLevelInterface.php | 7 +- src/Exception/ConnectionTimeoutException.php | 9 +- src/Exception/Exception.php | 18 ++ src/Exception/ExceptionInterface.php | 4 +- src/Exception/HandshakeException.php | 17 +- src/Exception/MessageLevelInterface.php | 2 +- src/Exception/ReconnectException.php | 10 +- src/Exception/RunnerException.php | 16 ++ src/Exception/ServerException.php | 4 +- src/Frame/Frame.php | 2 +- src/Frame/FrameHandler.php | 34 +++- src/Http/DefaultHttpFactory.php | 2 +- src/Message/Message.php | 8 +- src/Runtime/IdentityInterface.php | 2 +- src/Trait/ConfigurationTrait.php | 2 +- 43 files changed, 441 insertions(+), 323 deletions(-) create mode 100644 docs/Migrate_1_2.md create mode 100644 src/Exception/Exception.php create mode 100644 src/Exception/RunnerException.php diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5f0a00c..bb1d9a8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,7 +14,7 @@ Please provide a clear and concise description of the suspected issue. If possible, provide information - possibly including code snippets - on how to reproduce the issue. **Logs** -If possible, provide logs that indicate the issue. See https://github.com/Textalk/websocket-php/blob/master/docs/Examples.md#logger on how to use a logger. +If possible, provide logs that indicate the issue. See [Examples: Logger](../../docs/Examples.md#logger) on how to use a logger. **Versions** * Version of this library diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index ffca2a2..7150301 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -12,7 +12,7 @@ jobs: name: Unit test steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up PHP uses: shivammathur/setup-php@v2 with: @@ -24,7 +24,7 @@ jobs: id: composer-cache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -39,7 +39,7 @@ jobs: name: Code standard steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up PHP uses: shivammathur/setup-php@v2 with: @@ -51,7 +51,7 @@ jobs: id: composer-cache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -66,7 +66,7 @@ jobs: name: Code coverage steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up PHP uses: shivammathur/setup-php@v2 with: @@ -78,7 +78,7 @@ jobs: id: composer-cache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -86,14 +86,14 @@ jobs: - name: Install dependencies run: composer install --prefer-dist - name: Code coverage - run: vendor/bin/phpunit --coverage-clover coverage/clover.xml -d --min-coverage=100 + run: vendor/bin/phpunit --coverage-clover coverage/clover.xml --do-not-fail-on-phpunit-warning -d --min-coverage=100 stan: runs-on: ubuntu-latest name: Static analysis steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up PHP uses: shivammathur/setup-php@v2 with: @@ -105,7 +105,7 @@ jobs: id: composer-cache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/README.md b/README.md index 68a91fe..0dd7348 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,11 @@ and is maintained by Sören Jensen, who has been maintaining the original since * [Changelog](docs/Changelog.md) - The changelog of this repo * [Contributing](docs/Contributing.md) - Contributors and requirements * [Examples](docs/Examples.md) - Examples + +## Migration + +* [v1 -> v2](docs/Migrate_1_2.md) - How to migrate from v1 to v2 * [v2 -> v3](docs/Migrate_2_3.md) - How to migrate from v2 to v3 -* [v3 -> v4](docs/Migrate_3_4.md) - How to migrate from v3 to v4 ## Installing diff --git a/composer.json b/composer.json index 0a70a73..a9f0d9d 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,7 @@ ], "autoload": { "psr-4": { - "WebSocket\\": "src/", - "X\\": "src-x/" + "WebSocket\\": "src/" } }, "autoload-dev": { @@ -28,25 +27,24 @@ }, "require": { "php": "^8.1", - "phrity/http": "^1.0", + "nyholm/psr7": "^1.4", + "phrity/http": "^1.1", "phrity/net-uri": "^2.1", - "phrity/net-stream": "dev-ws4 as 2.4.0", + "phrity/net-stream": "^2.4", + "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0", "psr/log": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { - "guzzlehttp/psr7": "^2.0", "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0", "phrity/logger-console": "^1.0", - "phrity/net-mock": "dev-ws4 as 2.4.0", + "phrity/net-mock": "^2.4", "phrity/util-errorhandler": "^1.1", "robiningelbrecht/phpunit-coverage-tools": "^1.9", "squizlabs/php_codesniffer": "^4.0" }, - "suggest": { - "ext-zlib": "Required for per-message deflate compression", - "psr/http-factory-implementation": "Use a complete http-factory implementation", - "psr/log-implementation": "Use a logger implementation" + "suggests": { + "ext-zlib": "Required for per-message deflate compression" } } diff --git a/docs/Changelog.md b/docs/Changelog.md index 02f2362..2c1d168 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -2,23 +2,28 @@ # Websocket: Changelog -## `v4.0` +## `v3.7` > PHP version `^8.1` -### `4.0.0` +### `3.7.0` - * Shared stream observer, sharable across multiple cients and servers (@sirn-se) * Configuration class for various settings (@sirn-se) - * MessageEncodingException when compression fails (@sirn-se) - * General improvement of exceptions (@sirn-se) - * Many class local configuration setters removed (@sirn-se) - * Remove deprecated code (@sirn-se) + * Identity interface and implementation (@sirn-se) + * Using Nyholm PSR HTTP (intermediate solution) (@sirn-se) + * Runner class wraps stream-select handler (@sirn-se) + * StreamFactory as constructor option Client/Server (@sirn-se) + * Preparations for v4 (@sirn-se) ## `v3.6` > PHP version `^8.1` +### `3.6.2` + + * Deflate compression fuzzy header parsing fix (@sirn-se) + * More documentation (@sirn-se) + ### `3.6.1` * Fix typos and broken links (@pieterocp) diff --git a/docs/Class_Synopsis.md b/docs/Class_Synopsis.md index a60891a..aa20212 100644 --- a/docs/Class_Synopsis.md +++ b/docs/Class_Synopsis.md @@ -2,31 +2,13 @@ # Class Synopsis -Public API of library classes. +Public API of included classes. ```php abstract class WebSocket\Exception\Exception extends RuntimeException imlements WebSocket\Exception\ExceptionInterface { } -abstract class WebSocket\Http\Message imlements Psr\Http\Message\MessageInterface, Stringable -{ - use WebSocket\Trait\StringableTrait; - - public method getAsArray(): array; - public method getBody(): Psr\Http\Message\StreamInterface; - public method getHeader(string $name): array; - public method getHeaderLine(string $name): string; - public method getHeaders(): array; - public method getProtocolVersion(): string; - public method hasHeader(string $name): bool; - public method withAddedHeader(string $name, mixed $value): self; - public method withBody(Psr\Http\Message\StreamInterface $body): self; - public method withHeader(string $name, mixed $value): self; - public method withProtocolVersion(string $version): self; - public method withoutHeader(string $name): self; -} - abstract class WebSocket\Message\Message imlements Stringable { use WebSocket\Trait\StringableTrait; @@ -45,14 +27,14 @@ abstract class WebSocket\Message\Message imlements Stringable public method setPayload(string $payload = ""): void; } -class WebSocket\Client imlements Psr\Log\LoggerAwareInterface, Stringable +class WebSocket\Client imlements WebSocket\Runtime\IdentityInterface, Psr\Log\LoggerAwareInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\ListenerTrait; use WebSocket\Trait\SendMethodsTrait; use WebSocket\Trait\StringableTrait; - public method __construct(Psr\Http\Message\UriInterface|string $uri, Phrity\Net\StreamFactory|null $streamFactory = null, Phrity\Http\HttpFactory|null $httpFactory = null, WebSocket\Runtime\Watcher|null $watcher = null, WebSocket\Configuration|null $configuration = null); + public method __construct(Psr\Http\Message\UriInterface|string $uri, WebSocket\Configuration|null $configuration = null, Phrity\Net\StreamFactory|null $streamFactory = null); public method __toString(): string; public method addHeader(string $name, string $content): self; public method addMiddleware(WebSocket\Middleware\MiddlewareInterface $middleware): self; @@ -61,6 +43,8 @@ class WebSocket\Client imlements Psr\Log\LoggerAwareInterface, Stringable public method getContext(): Phrity\Net\Context; public method getFrameSize(): int; public method getHandshakeResponse(): Psr\Http\Message\ResponseInterface|null; + public method getIdentity(): string; + public method getMeta(string $key): mixed; public method getName(): string|null; public method getRemoteName(): string|null; public method getTimeout(): int|float; @@ -70,7 +54,7 @@ class WebSocket\Client imlements Psr\Log\LoggerAwareInterface, Stringable public method isWritable(): bool; public method receive(): WebSocket\Message\Message; public method send(WebSocket\Message\Message $message): WebSocket\Message\Message; - public method setContext(Phrity\Net\Context $context): self; + public method setContext(Phrity\Net\Context|array $context): self; public method setFrameSize(int $frameSize): self; public method setHttpFactory(Phrity\Http\HttpFactory $httpFactory): self; public method setLogger(Psr\Log\LoggerInterface $logger): void; @@ -101,7 +85,7 @@ class WebSocket\Configuration imlements Psr\Log\LoggerAwareInterface, Stringable public method setTimeout(int|float $timeout): void; } -class WebSocket\Connection imlements Stringable +class WebSocket\Connection imlements WebSocket\Runtime\IdentityInterface, Psr\Log\LoggerAwareInterface, Phrity\Net\StreamContainerInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\SendMethodsTrait; @@ -114,11 +98,15 @@ class WebSocket\Connection imlements Stringable public method closeWrite(): self; public method disconnect(): self; public method getContext(): Phrity\Net\Context; + public method getFrameSize(): int; public method getHandshakeRequest(): Psr\Http\Message\RequestInterface|null; public method getHandshakeResponse(): Psr\Http\Message\ResponseInterface|null; + public method getIdentity(): string; public method getMeta(string $key): mixed; - public method getName(): string; - public method getRemoteName(): string; + public method getName(): string|null; + public method getRemoteName(): string|null; + public method getStream(): Phrity\Net\SocketStream; + public method getTimeout(): int|float; public method isConnected(): bool; public method isReadable(): bool; public method isWritable(): bool; @@ -127,9 +115,12 @@ class WebSocket\Connection imlements Stringable public method pushHttp(Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; public method pushMessage(WebSocket\Message\Message $message): WebSocket\Message\Message; public method send(WebSocket\Message\Message $message): WebSocket\Message\Message; + public method setFrameSize(int $frameSize): self; public method setHandshakeRequest(Psr\Http\Message\RequestInterface $request): self; public method setHandshakeResponse(Psr\Http\Message\ResponseInterface $response): self; + public method setLogger(Psr\Log\LoggerInterface $logger): void; public method setMeta(string $key, mixed $value): void; + public method setTimeout(int|float $timeout): self; public method tick(): void; } @@ -180,6 +171,10 @@ class WebSocket\Exception\ReconnectException extends WebSocket\Exception\Excepti public method getUri(): Phrity\Net\Uri|null; } +class WebSocket\Exception\RunnerException extends WebSocket\Exception\Exception +{ +} + class WebSocket\Exception\ServerException extends WebSocket\Exception\Exception { } @@ -201,7 +196,7 @@ class WebSocket\Frame\Frame imlements Stringable public method setRsv1(bool $rsv1): void; } -class WebSocket\Frame\FrameHandler imlements Stringable +class WebSocket\Frame\FrameHandler imlements Psr\Log\LoggerAwareInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\OpcodeTrait; @@ -210,6 +205,7 @@ class WebSocket\Frame\FrameHandler imlements Stringable public method __construct(Phrity\Net\SocketStream $stream, bool $pushMasked, bool $pullMaskedRequired, WebSocket\Configuration|null $configuration = null); public method pull(): WebSocket\Frame\Frame; public method push(WebSocket\Frame\Frame $frame): int; + public method setLogger(Psr\Log\LoggerInterface $logger): void; } class WebSocket\Http\DefaultHttpFactory extends Phrity\Http\HttpFactory @@ -221,54 +217,29 @@ class WebSocket\Http\DefaultHttpFactory extends Phrity\Http\HttpFactory public method createUri(string $uri = ""): Psr\Http\Message\UriInterface; } -class WebSocket\Http\HttpHandler imlements Stringable +class WebSocket\Http\HttpHandler imlements Psr\Log\LoggerAwareInterface, Stringable { use WebSocket\Trait\StringableTrait; public method __construct(Phrity\Net\SocketStream $stream, bool $ssl = false, Phrity\Http\HttpFactory|null $httpFactory = null); public method pull(): Psr\Http\Message\MessageInterface; public method push(Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; + public method setLogger(Psr\Log\LoggerInterface $logger): void; } -class WebSocket\Http\Request extends WebSocket\Http\Message imlements Psr\Http\Message\RequestInterface +class WebSocket\Http\Request extends Nyholm\Psr7\Request imlements Psr\Http\Message\RequestInterface { public method __construct(string $method = "GET", Psr\Http\Message\UriInterface|string $uri = ""); - public method __toString(): string; - public method getAsArray(): array; - public method getMethod(): string; - public method getRequestTarget(): string; - public method getUri(): Psr\Http\Message\UriInterface; - public method withMethod(string $method): self; - public method withRequestTarget(mixed $requestTarget): self; - public method withUri(Psr\Http\Message\UriInterface $uri, bool $preserveHost = false): self; } -class WebSocket\Http\Response extends WebSocket\Http\Message imlements Psr\Http\Message\ResponseInterface +class WebSocket\Http\Response extends Nyholm\Psr7\Response imlements Psr\Http\Message\ResponseInterface { public method __construct(int $code = 200, string $reasonPhrase = ""); - public method __toString(): string; - public method getAsArray(): array; - public method getReasonPhrase(): string; - public method getStatusCode(): int; - public method withStatus(int $code, string $reasonPhrase = ""): self; } -class WebSocket\Http\ServerRequest extends WebSocket\Http\Request imlements Psr\Http\Message\ServerRequestInterface +class WebSocket\Http\ServerRequest extends Nyholm\Psr7\ServerRequest imlements Psr\Http\Message\ServerRequestInterface { - public method __toString(): string; - public method getAttribute(string $name, mixed $default = null); - public method getAttributes(): array; - public method getCookieParams(): array; - public method getParsedBody(); - public method getQueryParams(): array; - public method getServerParams(): array; - public method getUploadedFiles(): array; - public method withAttribute(string $name, mixed $value): self; - public method withCookieParams(array $cookies): self; - public method withParsedBody(mixed $data): self; - public method withQueryParams(array $query): self; - public method withUploadedFiles(array $uploadedFiles): self; - public method withoutAttribute(string $name): self; + public method __construct(string $method = "GET", Psr\Http\Message\UriInterface|string $uri = ""); } class WebSocket\Message\Binary extends WebSocket\Message\Message @@ -286,7 +257,7 @@ class WebSocket\Message\Close extends WebSocket\Message\Message public method setPayload(string $payload = ""): void; } -class WebSocket\Message\MessageHandler imlements Stringable +class WebSocket\Message\MessageHandler imlements Psr\Log\LoggerAwareInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; @@ -294,6 +265,7 @@ class WebSocket\Message\MessageHandler imlements Stringable public method __construct(WebSocket\Frame\FrameHandler $frameHandler, WebSocket\Configuration|null $configuration = null); public method pull(): WebSocket\Message\Message; public method push(WebSocket\Message\Message $message, int $size = WebSocket\Message\MessageHandler::DEFAULT_SIZE): WebSocket\Message\Message; + public method setLogger(Psr\Log\LoggerInterface $logger): void; } class WebSocket\Message\Ping extends WebSocket\Message\Message @@ -310,7 +282,7 @@ class WebSocket\Message\Text extends WebSocket\Message\Message public method setCompress(bool $compress): void; } -class WebSocket\Middleware\Callback imlements WebSocket\Middleware\ProcessHttpIncomingInterface, WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, WebSocket\Middleware\ProcessTickInterface, Stringable +class WebSocket\Middleware\Callback imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, WebSocket\Middleware\ProcessTickInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; @@ -321,9 +293,10 @@ class WebSocket\Middleware\Callback imlements WebSocket\Middleware\ProcessHttpIn public method processIncoming(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection): WebSocket\Message\Message; public method processOutgoing(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection, WebSocket\Message\Message $message): WebSocket\Message\Message; public method processTick(WebSocket\Middleware\ProcessTickStack $stack, WebSocket\Connection $connection): void; + public method setLogger(Psr\Log\LoggerInterface $logger): void; } -class WebSocket\Middleware\CloseHandler imlements WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, Stringable +class WebSocket\Middleware\CloseHandler imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; @@ -331,9 +304,10 @@ class WebSocket\Middleware\CloseHandler imlements WebSocket\Middleware\ProcessIn public method __construct(); public method processIncoming(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection): WebSocket\Message\Message; public method processOutgoing(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection, WebSocket\Message\Message $message): WebSocket\Message\Message; + public method setLogger(Psr\Log\LoggerInterface $logger): void; } -class WebSocket\Middleware\CompressionExtension imlements WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, Stringable +class WebSocket\Middleware\CompressionExtension imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; @@ -343,6 +317,7 @@ class WebSocket\Middleware\CompressionExtension imlements WebSocket\Middleware\P public method processHttpOutgoing(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection, Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; public method processIncoming(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection): WebSocket\Message\Message; public method processOutgoing(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection, WebSocket\Message\Message $message): WebSocket\Message\Message; + public method setLogger(Psr\Log\LoggerInterface $logger): void; } class WebSocket\Middleware\CompressionExtension\DeflateCompressor imlements WebSocket\Middleware\CompressionExtension\CompressorInterface, Stringable @@ -358,16 +333,17 @@ class WebSocket\Middleware\CompressionExtension\DeflateCompressor imlements WebS public method isEligable(object $configuration): bool; } -class WebSocket\Middleware\FollowRedirect imlements WebSocket\Middleware\ProcessHttpIncomingInterface, Stringable +class WebSocket\Middleware\FollowRedirect imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; public method __construct(int $limit = 10); public method processHttpIncoming(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection): Psr\Http\Message\MessageInterface; + public method setLogger(Psr\Log\LoggerInterface $logger): void; } -class WebSocket\Middleware\MiddlewareHandler imlements Stringable +class WebSocket\Middleware\MiddlewareHandler imlements Psr\Log\LoggerAwareInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; @@ -379,9 +355,10 @@ class WebSocket\Middleware\MiddlewareHandler imlements Stringable public method processIncoming(WebSocket\Connection $connection): WebSocket\Message\Message; public method processOutgoing(WebSocket\Connection $connection, WebSocket\Message\Message $message): WebSocket\Message\Message; public method processTick(WebSocket\Connection $connection): void; + public method setLogger(Psr\Log\LoggerInterface $logger): void; } -class WebSocket\Middleware\PingInterval imlements WebSocket\Middleware\ProcessOutgoingInterface, WebSocket\Middleware\ProcessTickInterface, Stringable +class WebSocket\Middleware\PingInterval imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessOutgoingInterface, WebSocket\Middleware\ProcessTickInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; @@ -389,15 +366,17 @@ class WebSocket\Middleware\PingInterval imlements WebSocket\Middleware\ProcessOu public method __construct(int|float|null $interval = null); public method processOutgoing(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection, WebSocket\Message\Message $message): WebSocket\Message\Message; public method processTick(WebSocket\Middleware\ProcessTickStack $stack, WebSocket\Connection $connection): void; + public method setLogger(Psr\Log\LoggerInterface $logger): void; } -class WebSocket\Middleware\PingResponder imlements WebSocket\Middleware\ProcessIncomingInterface, Stringable +class WebSocket\Middleware\PingResponder imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessIncomingInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; public method __construct(); public method processIncoming(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection): WebSocket\Message\Message; + public method setLogger(Psr\Log\LoggerInterface $logger): void; } class WebSocket\Middleware\ProcessHttpStack imlements Stringable @@ -426,7 +405,7 @@ class WebSocket\Middleware\ProcessTickStack imlements Stringable public method handleTick(): void; } -class WebSocket\Middleware\SubprotocolNegotiation imlements WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, Stringable +class WebSocket\Middleware\SubprotocolNegotiation imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; @@ -434,25 +413,26 @@ class WebSocket\Middleware\SubprotocolNegotiation imlements WebSocket\Middleware public method __construct(array $subprotocols, bool $require = false); public method processHttpIncoming(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection): Psr\Http\Message\MessageInterface; public method processHttpOutgoing(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection, Psr\Http\Message\MessageInterface $message): Psr\Http\Message\MessageInterface; + public method setLogger(Psr\Log\LoggerInterface $logger): void; } -class WebSocket\Runtime\Watcher +class WebSocket\Runtime\Runner { - public method __construct(Phrity\Net\StreamCollection $streamCollection); - public method attach(string $key, Phrity\Net\StreamInterface $attach, Closure $onSelect): void; - public method detach(string $key): void; - public method isEmpty(): bool; - public method watch(float $timeout): void; + public method __construct(Phrity\Net\StreamFactory $streamFactory); + public method attach(Phrity\Net\StreamContainerInterface $streamContainer, Closure $onSelect): void; + public method detach(string $identity): void; + public method handle(int|float $timeout): void; + public method select(int|float $timeout): Phrity\Net\StreamCollection; } -class WebSocket\Server imlements Psr\Log\LoggerAwareInterface, Stringable +class WebSocket\Server imlements WebSocket\Runtime\IdentityInterface, Psr\Log\LoggerAwareInterface, Phrity\Net\StreamContainerInterface, Stringable { use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\ListenerTrait; use WebSocket\Trait\SendMethodsTrait; use WebSocket\Trait\StringableTrait; - public method __construct(int $port = 80, bool $ssl = false, Phrity\Net\StreamFactory|null $streamFactory = null, Phrity\Http\HttpFactory|null $httpFactory = null, WebSocket\Runtime\Watcher|null $watcher = null, WebSocket\Configuration|null $configuration = null); + public method __construct(int $port = 80, bool $ssl = false, WebSocket\Configuration|null $configuration = null, Phrity\Net\StreamFactory|null $streamFactory = null); public method __toString(): string; public method addMiddleware(WebSocket\Middleware\MiddlewareInterface $middleware): self; public method disconnect(): void; @@ -460,15 +440,17 @@ class WebSocket\Server imlements Psr\Log\LoggerAwareInterface, Stringable public method getConnections(): array; public method getContext(): Phrity\Net\Context; public method getFrameSize(): int; + public method getIdentity(): string; public method getPort(): int; public method getReadableConnections(): array; public method getScheme(): string; + public method getStream(): Phrity\Net\SocketServer; public method getTimeout(): int|float; public method getWritableConnections(): array; public method isRunning(): bool; public method isSsl(): bool; public method send(WebSocket\Message\Message $message): WebSocket\Message\Message; - public method setContext(Phrity\Net\Context $context): self; + public method setContext(Phrity\Net\Context|array $context): self; public method setFrameSize(int $frameSize): self; public method setHttpFactory(Phrity\Http\HttpFactory $httpFactory): self; public method setLogger(Psr\Log\LoggerInterface $logger): void; @@ -510,7 +492,6 @@ inteface WebSocket\Middleware\CompressionExtension\CompressorInterface imlements inteface WebSocket\Middleware\MiddlewareInterface imlements Stringable { - public method setConfiguration(WebSocket\Configuration $configuration): self; } inteface WebSocket\Middleware\ProcessHttpIncomingInterface imlements WebSocket\Middleware\MiddlewareInterface @@ -538,6 +519,11 @@ inteface WebSocket\Middleware\ProcessTickInterface imlements WebSocket\Middlewar public method processTick(WebSocket\Middleware\ProcessTickStack $stack, WebSocket\Connection $connection): void; } +inteface WebSocket\Runtime\IdentityInterface +{ + public method getIdentity(): string; +} + trait WebSocket\Trait\ConfigurationTrait { public method getConfiguration(): WebSocket\Configuration; @@ -549,30 +535,9 @@ trait WebSocket\Trait\ListenerTrait { public method onBinary(Closure $closure): self; public method onClose(Closure $closure): self; + public method onConnect(Closure $closure): self; public method onDisconnect(Closure $closure): self; public method onError(Closure $closure): self; public method onHandshake(Closure $closure): self; - public method onPing(Closure $closure): self; - public method onPong(Closure $closure): self; - public method onText(Closure $closure): self; - public method onTick(Closure $closure): self; -} - -trait WebSocket\Trait\OpcodeTrait -{ -} - -trait WebSocket\Trait\SendMethodsTrait -{ - public method binary(string $message): WebSocket\Message\Binary; - public method close(int $status = 1000, string $message = "ttfn"): WebSocket\Message\Close; - public method ping(string $message = ""): WebSocket\Message\Ping; - public method pong(string $message = ""): WebSocket\Message\Pong; - public method text(string $message): WebSocket\Message\Text; -} - -trait WebSocket\Trait\StringableTrait -{ - public method __toString(): string; } -``` \ No newline at end of file +``` diff --git a/docs/Client.md b/docs/Client.md index 8fd1165..494bb0b 100644 --- a/docs/Client.md +++ b/docs/Client.md @@ -139,9 +139,17 @@ $client->close(1000, "Closing now"); ## Configuration -The Client takes one argument: [URI](http://tools.ietf.org/html/rfc3986) as a class implementing [UriInterface](https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface) or as string. +The Client has one required argument: [URI](http://tools.ietf.org/html/rfc3986) as a class implementing [UriInterface](https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface) or as string. The client support `ws` (`tcp`) and `wss` (`ssl`) schemas, depending on SSL configuration. +```php +__construct( + Psr\Http\Message\UriInterface|string $uri, + WebSocket\Configuration|null $configuration = null, + Phrity\Net\StreamFactory|null $streamFactory = null, +); +``` + Other options are available using the Configuration class. - Logger @@ -154,8 +162,8 @@ Read more on [Configuration](Configuration.md). ### HTTP factories -By default the Client uses a minimal [PSR-7 HTTP message](https://www.php-fig.org/psr/psr-7/) implementation. -Other (more complete) implementations can be used by setting [PSR-17 HTTP factories](https://www.php-fig.org/psr/psr-17/) on the Client. +By default the Client wraps a [PSR-7 HTTP message](https://www.php-fig.org/psr/psr-7/) implementation. +Other implementations can be used by setting [PSR-17 HTTP factories](https://www.php-fig.org/psr/psr-17/) on the Client. Set a configured HttpFactory class on the Client. ```php diff --git a/docs/Configuration.md b/docs/Configuration.md index 877e391..382025d 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -3,7 +3,6 @@ # Websocket: Configuration The Configuration class is used to configure Client, Server and various worker classes. -It is delegated throughout class hierarchy unless explicitly set. ## Using Configuration instance @@ -36,6 +35,7 @@ Get and set Configuration; ```php $configuration = $client->getConfiguration(); $client->setConfiguration($configuration); + $configuration = $server->getConfiguration(); $server->setConfiguration($configuration); ``` @@ -50,7 +50,7 @@ type: Psr\Log\LoggerInterface default: Psr\Log\NullLogger ``` -Attach any [PSR-4 compatible](https://www.php-fig.org/psr/psr-3/) logger. +Attach any [PSR-3 compatible](https://www.php-fig.org/psr/psr-3/) logger. ```php // Configuration instance @@ -105,7 +105,7 @@ default: 60 Timeout for various operations can be specified in seconds. This affects how long Client and Server will wait for connection, read and write operations, and listener scope. Default is `60` seconds. Minimum is `0` seconds. Accepts int or float value. -Avoid setting very low values as it will cause a read loop to use all +Avoid setting very low values as it will cause a read loop that uses lots of the available processing power even when there's nothing to read. ```php @@ -128,7 +128,7 @@ type: int<1, max> default: 4096 ``` -Defines the maximum payload per frame size in bytes. +Defines the maximum frame payload size in bytes. Default is `4096` bytes. Minimum is `1` byte. Do not change unless you have a strong reason to do so. diff --git a/docs/Connection.md b/docs/Connection.md index c56a41c..0da4a8e 100644 --- a/docs/Connection.md +++ b/docs/Connection.md @@ -80,18 +80,41 @@ $connection->getHandshakeResponse(); ## Configuration -Configuration is normally inherited from Client or Sever, but can also be explicitly set. +Some options are available runtime by calling configuration methods. + +### Logger + +Connection support adding any [PSR-4 compatible](https://www.php-fig.org/psr/psr-3/) logger. + ```php -$configuration = $connection->getConfiguration(); -$connection->setConfiguration($configuration); +$connection->setLogger(Psr\Log\LoggerInterface $logger); ``` -- Logger -- Timeout +### Timeout -Read more on [Configuration](Configuration.md). +Timeout for various operations can be specified in seconds. +This affects how long a Connection will wait for read and write operations. +Default is `60` seconds. Minimum is `0` seconds. Accepts int or float value. +Avoid setting very low values as it will cause a read loop to use all +available processing power even when there's nothing to read. + +```php +$connection->setTimeout(300); // set timeout in seconds +$connection->getTimeout(); // => current timeout in seconds +``` + +### Frame size + +Defines the maximum payload per frame size in bytes. +Default is `4096` bytes. Minimum is `1` byte. +Do not change unless you have a strong reason to do so. + +```php +$connection->setFrameSize(1024); // set maximum payload frame size in bytes +$connection->getFrameSize(); // => current maximum payload frame size in bytes +``` -## Context +### Context Connection exposes [context options and parameters](https://www.php.net/manual/en/context.php) using the [Phrity\Net\Context](https://github.com/sirn-se/phrity-net-stream?tab=readme-ov-file#context-class) class. diff --git a/docs/Contributing.md b/docs/Contributing.md index 7c816e1..b63987a 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -104,4 +104,4 @@ make stan * Pieter Oliver * Sebastian Hagens * Adrian Mihai -* Pieter Oliver +* Pieter Oliver \ No newline at end of file diff --git a/docs/Examples.md b/docs/Examples.md index f2e58f1..83f8e74 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -8,7 +8,7 @@ Here are some examples on how to use the WebSocket library. Client and Server accepts a [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger to be attached. -The `phrity/logger-console` in example is useful for debugging. +The ConsoleLogger in example is useful for debugging. For production, use a logger that stores or accumulates log data. ```php @@ -189,24 +189,19 @@ use WebSocket\Server; $port = 443; // Port to use $logger = new Logger(); // PSR-3 Logger to use -// Create context -$context = new Phrity\Net\Context(); -// Set up SSL with CA certificate -$context->setOptions(['ssl' => [ - 'local_cert' => 'certs/CA/acs-ws-server.crt', - 'local_pk' => 'certs/CA/acs-ws-server.key', - 'cafile' => 'certs/CA/ca.crt', - 'verify_peer' => true, // if false, accept SSL handshake without client certificate - 'verify_peer_name' => false, - 'allow_self_signed' => false, - 'capture_peer_cert' => true, -]]); - // Create server $server = new Server(port: $port, ssl: true); $server - // Add the context - ->setContext($context) + // Set up SSL with CA certificate + ->setContext(['ssl' => [ + 'local_cert' => 'certs/CA/acs-ws-server.crt', + 'local_pk' => 'certs/CA/acs-ws-server.key', + 'cafile' => 'certs/CA/ca.crt', + 'verify_peer' => true, // if false, accept SSL handshake without client certificate + 'verify_peer_name' => false, + 'allow_self_signed' => false, + 'capture_peer_cert' => true, + ]]) // Add standard middlewares ->addMiddleware(new CloseHandler()) ->addMiddleware(new PingResponder()) diff --git a/docs/Index.md b/docs/Index.md index f356646..a526781 100644 --- a/docs/Index.md +++ b/docs/Index.md @@ -9,7 +9,6 @@ * [Configuration](Configuration.md) - Configuration for Client and Server * [Connection](Connection.md) - The connection between client and server -* [Error handling](ErrorHandling.md) - Notes on exceptions and error handling * [Listener](Listener.md) - Listeners allow callbacks when messages are received * [Message](Message.md) - Represents a WebSocket message being sent or received * [Middleware](Middleware.md) - Middlewares allow implementations to act incoming and outgoing messages @@ -24,5 +23,5 @@ ## Migration +* [v1 -> v2](Migrate_1_2.md) - How to migrate from v1 to v2 * [v2 -> v3](Migrate_2_3.md) - How to migrate from v2 to v3 -* [v3 -> v4](Migrate_3_4.md) - How to migrate from v3 to v4 diff --git a/docs/Migrate_1_2.md b/docs/Migrate_1_2.md new file mode 100644 index 0000000..36073d1 --- /dev/null +++ b/docs/Migrate_1_2.md @@ -0,0 +1,56 @@ +[Documentation](Index.md) / Migration v1 -> v2 + +# Websocket: Migration v1 -> v2 + +Version `2.x` has significant changes compared to previous version. +Amongst other things, it introduces callback listeners, middleware support, and the Server now support multiple connections. + +## Constructor and configuration + +Where `1.x` was configured by using an array of options on the constructor, +`2.x` provides a number of configuration methods instead. +Replace configuration with a call to corresponding method instead. + +```php +new WebSocket\Client(string $uri) +new WebSocket\Server(bool $ssl, int $port) + +setLogger(Psr\Log\LoggerInterface $logger) // logger +setTimeout(int $timeout) // timeout +setFrameSize(int $frameSize) // fragment_size +setPersistent(bool $persistent) // persistent +setContext(array $context_as_array) // context +addHeader(string $name, string $value) // headers +``` + +The `filter` and `return_obj` are no longer valid. + +## Middlewares + +`2.x` introduces middlewares to extend functionality to Client and Server. +Two of the middlewares cover functionality built-in in `1.x` and should be added unless you create your own code instead. + +```php +addMiddleware(new WebSocket\Middleware\CloseHandler()) +addMiddleware(new WebSocket\Middleware\PingResponder()) +``` + +## Receiving messages + +The Client `receive()` method always return an instance of Message (Text, Binary, Ping, Pong or Close). +This corresponds to setting `return_obj: true` in `1.x` configuration. By default `1.x` returned string. + +You are encouraged to read incoming messages using [listener callback methods](https://github.com/sirn-se/websocket-php/blob/2.0.0/docs/Client.md#subscribe-operation) instead. + +The Server no longer has the `receive()` method. +Receiving, use [listener callback methods](https://github.com/sirn-se/websocket-php/blob/2.0.0/docs/Server.md#message-listeners). + +## Sending messages + +The `send(...)` method no longer accepts message and opcode as strings, +but only accepts an instance of Message (Text, Binary, Ping, Pong or Close). +However, there are convenience methods available as `text(...)`, `binary(...)`, `ping(...)`, `pong(...)` and `close(...)`. + +## More? + +As `2.x` has significant internal code changes, other classes and methods are likely to have changed. diff --git a/docs/Server.md b/docs/Server.md index c209bb8..06f29f9 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -136,9 +136,18 @@ $server->close(1000, "Closing now"); ## Configuration -The Server takes two arguments; port and ssl. +The Server has two main arguments; port and ssl. By default, ssl is false. If port is not specified, it will use 80 for non-secure and 443 for secure server. +```php +__construct( + int $port = 80, + bool $ssl = false, + WebSocket\Configuration|null $configuration = null, + Phrity\Net\StreamFactory|null $streamFactory = null, +); +``` + Other options are available using the Configuration class. - Logger @@ -151,8 +160,8 @@ Read more on [Configuration](Configuration.md). ### HTTP factories -By default the Server uses a minimal [PSR-7 HTTP message](https://www.php-fig.org/psr/psr-7/) implementation. -Other (more complete) implementations can be used by setting [PSR-17 HTTP factories](https://www.php-fig.org/psr/psr-17/) on the Server. +By default the Server wraps a [PSR-7 HTTP message](https://www.php-fig.org/psr/psr-7/) implementation. +Other implementations can be used by setting [PSR-17 HTTP factories](https://www.php-fig.org/psr/psr-17/) on the Server. Set a configured HttpFactory class on the Client. ```php diff --git a/docu.json b/docu.json index 609dd44..0968052 100644 --- a/docu.json +++ b/docu.json @@ -25,10 +25,6 @@ "name": "Connection", "source": "docs/Connection.md" }, - { - "name": "Error handling", - "source": "docs/ErrorHandling.md" - }, { "name": "Listener", "source": "docs/Listener.md" @@ -74,7 +70,7 @@ "source": "docs/Middleware/Creating.md" } ] - }, + } ], "name": "Developer resources", "content": [ @@ -98,12 +94,12 @@ "name": "Migration", "content": [ { - "name": "Migration v2 -> v3", - "source": "docs/Migrate_2_3.md" + "name": "Migration v1 -> v2", + "source": "docs/Migrate_1_2.md" }, { - "name": "Migration v3 -> v4", - "source": "docs/Migrate_3_4.md" + "name": "Migration v2 -> v3", + "source": "docs/Migrate_2_3.md" } ] }, diff --git a/examples/delegating_server.php b/examples/delegating_server.php index 3ab5824..dc9dbfb 100644 --- a/examples/delegating_server.php +++ b/examples/delegating_server.php @@ -1,7 +1,7 @@ setTimeout($options['timeout']); + echo "# Set timeout: {$options['timeout']}\n"; + if (class_exists(ConsoleLogger::class)) { + $logger = new ConsoleLogger(format: '{level} | {message}', cliOptions: true); + $configuration->setLogger($logger); + echo "# Using logger\n"; + } + if (isset($options['framesize'])) { + $configuration->setFrameSize($options['framesize']); + echo "# Set frame size: {$options['framesize']}\n"; + } + if (isset($options['connections'])) { + $configuration->setMaxConnections($options['connections']); + echo "# Set max connections: {$options['connections']}\n"; + } + // Initiate server echo "# Setting up Server\n"; - $server = new Server($options['port'], isset($options['ssl'])); + $server = new Server($options['port'], isset($options['ssl']), $configuration); $server ->addMiddleware(new CloseHandler()) ->addMiddleware(new PingResponder()) - ->setTimeout($options['timeout']) ; // Initiate client echo "# Setting up Client\n"; - $client = new Client($options['remote']); + $client = new Client($options['remote'], $configuration); $client ->addMiddleware(new CloseHandler()) ->addMiddleware(new PingResponder()) ->addMiddleware(new PingInterval(30)) - ->setTimeout($options['timeout']) ; - // Configuration - echo "# Set timeout: {$options['timeout']}\n"; - if (class_exists(ConsoleLogger::class)) { - $logger = new ConsoleLogger(format: '{level} | {message}', cliOptions: true); - $server->setLogger($logger); - $client->setLogger($logger); - echo "# Using logger\n"; - } - if (isset($options['framesize'])) { - $server->setFrameSize($options['framesize']); - $client->setFrameSize($options['framesize']); - echo "# Set frame size: {$options['framesize']}\n"; - } - if (isset($options['connections'])) { - $server->getConfiguration()->setMaxConnections($options['connections']); - echo "# Set max connections: {$options['connections']}\n"; - } if (isset($options['deflate'])) { $server->addMiddleware(new CompressionExtension(new DeflateCompressor())); $client->addMiddleware(new CompressionExtension(new DeflateCompressor())); @@ -148,6 +147,8 @@ $client->stop(); }); + echo "# Listening on port {$server->getPort()}\n"; + // Run loop // @phpstan-ignore while.alwaysTrue while (true) { @@ -155,5 +156,5 @@ $server->start(); } } catch (Throwable $e) { - echo "# ERROR: {$e->getMessage()}\n"; + echo "# ERROR: {$e->getMessage()}\n $e \n"; } diff --git a/examples/echoserver.php b/examples/echoserver.php index 51063cb..b845491 100644 --- a/examples/echoserver.php +++ b/examples/echoserver.php @@ -1,7 +1,7 @@ addMiddleware(new CloseHandler()) - ->addMiddleware(new PingResponder()) - ; - - // If debug mode and logger is available + // Configuration + $configuration = new Configuration(); if (class_exists(ConsoleLogger::class)) { - $server->setLogger(new ConsoleLogger(format: '{level} | {message}', cliOptions: true)); + $configuration->setLogger(new ConsoleLogger(format: '{level} | {message}', cliOptions: true)); echo "# Using logger\n"; } if (isset($options['timeout'])) { - $server->setTimeout($options['timeout']); + $configuration->setTimeout($options['timeout']); echo "# Set timeout: {$options['timeout']}\n"; } if (isset($options['framesize'])) { - $server->setFrameSize($options['framesize']); + $configuration->setFrameSize($options['framesize']); echo "# Set frame size: {$options['framesize']}\n"; } if (isset($options['connections'])) { - $server->getConfiguration()->setMaxConnections($options['connections']); + $configuration->setMaxConnections($options['connections']); echo "# Set max connections: {$options['connections']}\n"; } + + $server = new Server($options['port'], isset($options['ssl']), $configuration); + $server + ->addMiddleware(new CloseHandler()) + ->addMiddleware(new PingResponder()) + ; + if (isset($options['deflate'])) { $server->addMiddleware(new CompressionExtension(new DeflateCompressor())); echo "# Using per-message: deflate compression\n"; diff --git a/examples/random_client.php b/examples/random_client.php index e448501..230f697 100644 --- a/examples/random_client.php +++ b/examples/random_client.php @@ -1,7 +1,7 @@ addMiddleware(new CloseHandler()) - ->addMiddleware(new PingResponder()) - ; - - if (isset($options['deflate'])) { - $client->addMiddleware(new CompressionExtension(new DeflateCompressor())); - echo "# Using per-message: deflate compression\n"; - } - - // If debug mode and logger is available + // Configuration + $configuration = new Configuration(); if (class_exists(ConsoleLogger::class)) { - $client->setLogger(new ConsoleLogger(format: '{level} | {message}', cliOptions: true)); + $configuration->setLogger(new ConsoleLogger(format: '{level} | {message}', cliOptions: true)); echo "# Using logger\n"; } if (isset($options['timeout'])) { - $client->setTimeout($options['timeout']); + $configuration->setTimeout($options['timeout']); echo "# Set timeout: {$options['timeout']}\n"; } if (isset($options['framesize'])) { - $client->setFrameSize($options['framesize']); + $configuration->setFrameSize($options['framesize']); echo "# Set frame size: {$options['framesize']}\n"; } + $client = new Client($options['uri'], $configuration); + $client + ->addMiddleware(new CloseHandler()) + ->addMiddleware(new PingResponder()) + ; + + if (isset($options['deflate'])) { + $client->addMiddleware(new CompressionExtension(new DeflateCompressor())); + echo "# Using per-message: deflate compression\n"; + } + echo "# Listening on {$options['uri']}\n"; $client->onHandshake(function (Client $client, Connection $connection, $request, $response) { echo "> [{$connection->getRemoteName()}] Client connected {$response->getStatusCode()}\n"; diff --git a/examples/random_server.php b/examples/random_server.php index 82bce26..2a3b0c4 100644 --- a/examples/random_server.php +++ b/examples/random_server.php @@ -1,7 +1,7 @@ addMiddleware(new CloseHandler()) - ->addMiddleware(new PingResponder()) - ; - - // If debug mode and logger is available + // Configuration + $configuration = new Configuration(); if (class_exists(ConsoleLogger::class)) { - $server->setLogger(new ConsoleLogger(format: '{level} | {message}', cliOptions: true)); + $configuration->setLogger(new ConsoleLogger(format: '{level} | {message}', cliOptions: true)); echo "# Using logger\n"; } if (isset($options['timeout'])) { - $server->setTimeout($options['timeout']); + $configuration->setTimeout($options['timeout']); echo "# Set timeout: {$options['timeout']}\n"; } if (isset($options['framesize'])) { - $server->setFrameSize($options['framesize']); + $configuration->setFrameSize($options['framesize']); echo "# Set frame size: {$options['framesize']}\n"; } if (isset($options['connections'])) { - $server->getConfiguration()->setMaxConnections($options['connections']); + $configuration->setMaxConnections($options['connections']); echo "# Set max connections: {$options['connections']}\n"; } + + $server = new Server($options['port'], isset($options['ssl']), $configuration); + $server + ->addMiddleware(new CloseHandler()) + ->addMiddleware(new PingResponder()) + ; + if (isset($options['deflate'])) { $server->addMiddleware(new CompressionExtension(new DeflateCompressor())); echo "# Using per-message: deflate compression\n"; diff --git a/examples/send.php b/examples/send.php index 5d80561..fa8dacc 100644 --- a/examples/send.php +++ b/examples/send.php @@ -1,7 +1,7 @@ setLogger(new ConsoleLogger(format: '{level} | {message}', cliOptions: true)); + echo "# Using logger\n"; + } + if (isset($options['timeout'])) { + $configuration->setTimeout($options['timeout']); + echo "# Set timeout: {$options['timeout']}\n"; + } + if (isset($options['framesize'])) { + $configuration->setFrameSize($options['framesize']); + echo "# Set frame size: {$options['framesize']}\n"; + } + + $client = new Client($options['uri'], $configuration); $client ->addMiddleware(new CloseHandler()) ->addMiddleware(new PingResponder()) @@ -90,20 +105,6 @@ echo "# Using per-message: deflate compression\n"; } - // If debug mode and logger is available - if (class_exists(ConsoleLogger::class)) { - $client->setLogger(new ConsoleLogger(format: '{level} | {message}', cliOptions: true)); - echo "# Using logger\n"; - } - if (isset($options['timeout'])) { - $client->setTimeout($options['timeout']); - echo "# Set timeout: {$options['timeout']}\n"; - } - if (isset($options['framesize'])) { - $client->setFrameSize($options['framesize']); - echo "# Set frame size: {$options['framesize']}\n"; - } - $type = $options['opcode']; $message = $client->$type($message); echo "< Sent '{$message->getContent()}' [opcode: {$message->getOpcode()}]\n"; diff --git a/src/Configuration.php b/src/Configuration.php index bff4e36..e90a988 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -1,7 +1,7 @@ status = $status; - parent::__construct($message); + parent::__construct($content); } public function getCloseStatus(): int diff --git a/src/Exception/ConnectionClosedException.php b/src/Exception/ConnectionClosedException.php index f3bc394..e8f423c 100644 --- a/src/Exception/ConnectionClosedException.php +++ b/src/Exception/ConnectionClosedException.php @@ -1,7 +1,7 @@ response = $response; - parent::__construct($connection, $message); } public function getResponse(): ResponseInterface diff --git a/src/Exception/MessageLevelInterface.php b/src/Exception/MessageLevelInterface.php index 5451fd1..094d862 100644 --- a/src/Exception/MessageLevelInterface.php +++ b/src/Exception/MessageLevelInterface.php @@ -1,7 +1,7 @@ uri = $uri; - parent::__construct($message); + parent::__construct("Reconnect requested" . ($uri ? ": {$uri}" : '')); } public function getUri(): Uri|null diff --git a/src/Exception/RunnerException.php b/src/Exception/RunnerException.php new file mode 100644 index 0000000..ec6f540 --- /dev/null +++ b/src/Exception/RunnerException.php @@ -0,0 +1,16 @@ +stream = $stream; $this->pushMasked = $pushMasked; @@ -44,6 +50,16 @@ public function __construct( $this->initConfiguration($configuration); } + /** + * Set logger. + * @param LoggerInterface $logger Logger implementation + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + } + /** * Pull frame from stream * @throws CloseException @@ -100,14 +116,17 @@ public function pull(): Frame } $frame = new Frame($opcode, $payload, $final, $rsv1, $rsv2, $rsv3); - $this->configuration->getLogger()->debug('[frame-handler] Pulled {opcode} frame', [ + $this->configuration->getLogger()->debug("[{scope}] Pulled '{opcode}' frame", [ + 'scope' => self::SCOPE, 'opcode' => $frame->getOpcode(), 'final' => $frame->isFinal(), 'content-length' => $frame->getPayloadLength(), ]); - if ($this->pullMaskedRequired && !$masked) { - $this->configuration->getLogger()->debug('[frame-handler] Masking required, but frame was unmasked'); + $this->configuration->getLogger()->error("[{scope}] Masking required, but frame was unmasked", [ + 'scope' => self::SCOPE, + 'opcode' => $frame->getOpcode(), + ]); throw new CloseException(1002, 'Masking required'); } @@ -162,7 +181,8 @@ public function push(Frame $frame): int // Write to stream. $written = $this->write($data); - $this->configuration->getLogger()->debug('[frame-handler] Pushed {opcode} frame', [ + $this->configuration->getLogger()->debug("[{scope}] Pushed '{opcode}' frame", [ + 'scope' => self::SCOPE, 'opcode' => $frame->getOpcode(), 'final' => $frame->isFinal(), 'content-length' => $frame->getPayloadLength(), diff --git a/src/Http/DefaultHttpFactory.php b/src/Http/DefaultHttpFactory.php index fd0e5c3..83595ea 100644 --- a/src/Http/DefaultHttpFactory.php +++ b/src/Http/DefaultHttpFactory.php @@ -1,7 +1,7 @@