From 45a8d255f3786dae6aa786387b8c5e136fe91e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Jensen?= Date: Tue, 30 Dec 2025 13:06:01 +0100 Subject: [PATCH] Configuration, logging, documentation --- composer.json | 1 + docs/Changelog.md | 11 + docs/Class_Synopsis.md | 80 ++- docs/Client.md | 112 ++-- docs/Configuration.md | 216 ++++++++ docs/Index.md | 1 + docs/Server.md | 67 +-- docs/tmp | 515 ------------------ docu.json | 18 +- examples/delegating_server.php | 47 +- examples/echoserver.php | 30 +- examples/random_client.php | 31 +- examples/random_server.php | 26 +- examples/send.php | 31 +- src/Client.php | 187 +++++-- src/Configuration.php | 195 +++++++ src/Connection.php | 107 +++- src/Frame/FrameHandler.php | 44 +- src/Message/MessageHandler.php | 25 +- src/Middleware/Callback.php | 26 +- src/Middleware/CloseHandler.php | 52 +- src/Middleware/CompressionExtension.php | 48 +- src/Middleware/FollowRedirect.php | 41 +- src/Middleware/MiddlewareHandler.php | 70 ++- src/Middleware/PingInterval.php | 33 +- src/Middleware/PingResponder.php | 26 +- src/Middleware/ProcessStack.php | 2 +- src/Middleware/SubprotocolNegotiation.php | 60 +- src/Server.php | 192 +++++-- src/Trait/ConfigurationTrait.php | 35 ++ src/Trait/LoggerAwareTrait.php | 44 -- tests/mock/MockStreamTrait.php | 7 +- tests/suites/client/ClientErrorTest.php | 2 +- tests/suites/client/ConfigTest.php | 18 + .../configuration/ConfigurationTest.php | 108 ++++ tests/suites/connection/ConnectionTest.php | 13 +- tests/suites/connection/ExceptionTest.php | 11 + tests/suites/middleware/CallbackTest.php | 5 + tests/suites/middleware/CloseHandlerTest.php | 2 + .../middleware/DeflateCompressorTest.php | 7 + .../suites/middleware/FollowRedirectTest.php | 3 + tests/suites/middleware/PingIntervalTest.php | 1 + tests/suites/middleware/PingResponderTest.php | 1 + tests/suites/middleware/ProcessStackTest.php | 2 + .../middleware/SubprotocolNegotiationTest.php | 6 + tests/suites/server/ConfigErrorTest.php | 1 + tests/suites/server/ConfigTest.php | 21 +- tests/suites/server/ServerTest.php | 3 +- 48 files changed, 1531 insertions(+), 1053 deletions(-) create mode 100644 docs/Configuration.md delete mode 100644 docs/tmp create mode 100644 src/Configuration.php create mode 100644 src/Trait/ConfigurationTrait.php delete mode 100644 src/Trait/LoggerAwareTrait.php create mode 100644 tests/suites/configuration/ConfigurationTest.php diff --git a/composer.json b/composer.json index 0e5b797..779238f 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "phrity/http": "^1.1", "phrity/net-uri": "^2.1", "phrity/net-stream": "^2.3", + "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0", "psr/log": "^1.0 || ^2.0 || ^3.0" }, diff --git a/docs/Changelog.md b/docs/Changelog.md index 5c584d6..26a31a9 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -2,6 +2,17 @@ # Websocket: Changelog +## `v3.7` + + > PHP version `^8.1` + +### `3.7.0` + + * Configuration class for various settings (@sirn-se) + * Identity interface and implementation (@sirn-se) + * Using Nyholm PSR HTTP (intermediate solution) (@sirn-se) + * Preparations for v4 (@sirn-se) + ## `v3.6` > PHP version `^8.1` diff --git a/docs/Class_Synopsis.md b/docs/Class_Synopsis.md index 22ae303..53c148c 100644 --- a/docs/Class_Synopsis.md +++ b/docs/Class_Synopsis.md @@ -29,12 +29,12 @@ abstract class WebSocket\Message\Message imlements Stringable class WebSocket\Client imlements WebSocket\Runtime\IdentityInterface, Psr\Log\LoggerAwareInterface, Stringable { + use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\ListenerTrait; - use WebSocket\Trait\LoggerAwareTrait; use WebSocket\Trait\SendMethodsTrait; use WebSocket\Trait\StringableTrait; - public method __construct(Psr\Http\Message\UriInterface|string $uri); + public method __construct(Psr\Http\Message\UriInterface|string $uri, 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; @@ -65,13 +65,33 @@ class WebSocket\Client imlements WebSocket\Runtime\IdentityInterface, Psr\Log\Lo public method stop(): void; } +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; +} + class WebSocket\Connection imlements WebSocket\Runtime\IdentityInterface, Psr\Log\LoggerAwareInterface, Stringable { - use WebSocket\Trait\LoggerAwareTrait; + 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); + 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 __destruct(); public method __toString(): string; public method addMiddleware(WebSocket\Middleware\MiddlewareInterface $middleware): self; @@ -174,13 +194,14 @@ class WebSocket\Frame\Frame imlements Stringable class WebSocket\Frame\FrameHandler imlements Psr\Log\LoggerAwareInterface, Stringable { - use WebSocket\Trait\LoggerAwareTrait; + use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\OpcodeTrait; use WebSocket\Trait\StringableTrait; - public method __construct(Phrity\Net\SocketStream $stream, bool $pushMasked, bool $pullMaskedRequired); + 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 @@ -234,10 +255,10 @@ class WebSocket\Message\Close extends WebSocket\Message\Message class WebSocket\Message\MessageHandler imlements Psr\Log\LoggerAwareInterface, Stringable { - use WebSocket\Trait\LoggerAwareTrait; + use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; - public method __construct(WebSocket\Frame\FrameHandler $frameHandler); + 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; @@ -259,7 +280,7 @@ class WebSocket\Message\Text extends WebSocket\Message\Message 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\LoggerAwareTrait; + 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); @@ -268,21 +289,23 @@ class WebSocket\Middleware\Callback imlements Psr\Log\LoggerAwareInterface, WebS 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 Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, Stringable { - use WebSocket\Trait\LoggerAwareTrait; + 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 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 Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, Stringable { - use WebSocket\Trait\LoggerAwareTrait; + use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; public method __construct(WebSocket\Middleware\CompressionExtension\CompressorInterface $compressors); @@ -290,6 +313,7 @@ class WebSocket\Middleware\CompressionExtension imlements Psr\Log\LoggerAwareInt 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 @@ -307,19 +331,20 @@ class WebSocket\Middleware\CompressionExtension\DeflateCompressor imlements WebS class WebSocket\Middleware\FollowRedirect imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, Stringable { - use WebSocket\Trait\LoggerAwareTrait; + 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 Psr\Log\LoggerAwareInterface, Stringable { - use WebSocket\Trait\LoggerAwareTrait; + use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; - public method __construct(WebSocket\Message\MessageHandler $messageHandler, WebSocket\Http\HttpHandler $httpHandler); + 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; @@ -331,21 +356,23 @@ class WebSocket\Middleware\MiddlewareHandler imlements Psr\Log\LoggerAwareInterf class WebSocket\Middleware\PingInterval imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessOutgoingInterface, WebSocket\Middleware\ProcessTickInterface, Stringable { - use WebSocket\Trait\LoggerAwareTrait; + use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\StringableTrait; 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 Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessIncomingInterface, Stringable { - use WebSocket\Trait\LoggerAwareTrait; + 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 @@ -376,22 +403,23 @@ class WebSocket\Middleware\ProcessTickStack imlements Stringable class WebSocket\Middleware\SubprotocolNegotiation imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, Stringable { - use WebSocket\Trait\LoggerAwareTrait; + 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; + public method setLogger(Psr\Log\LoggerInterface $logger): void; } class WebSocket\Server imlements WebSocket\Runtime\IdentityInterface, Psr\Log\LoggerAwareInterface, Stringable { + use WebSocket\Trait\ConfigurationTrait; use WebSocket\Trait\ListenerTrait; - use WebSocket\Trait\LoggerAwareTrait; use WebSocket\Trait\SendMethodsTrait; use WebSocket\Trait\StringableTrait; - public method __construct(int $port = 80, bool $ssl = false); + public method __construct(int $port = 80, bool $ssl = false, WebSocket\Configuration|null $configuration = null); public method __toString(): string; public method addMiddleware(WebSocket\Middleware\MiddlewareInterface $middleware): self; public method disconnect(): void; @@ -482,6 +510,13 @@ inteface WebSocket\Runtime\IdentityInterface public method getIdentity(): string; } +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; +} + trait WebSocket\Trait\ListenerTrait { public method onBinary(Closure $closure): self; @@ -496,13 +531,6 @@ trait WebSocket\Trait\ListenerTrait public method onTick(Closure $closure): self; } -trait WebSocket\Trait\LoggerAwareTrait -{ - public method attachLogger(mixed $instance): void; - public method initLogger(Psr\Log\LoggerInterface|null $logger = null): void; - public method setLogger(Psr\Log\LoggerInterface $logger): void; -} - trait WebSocket\Trait\OpcodeTrait { } diff --git a/docs/Client.md b/docs/Client.md index c187910..f2cbc64 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,71 +141,21 @@ $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 -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 new file mode 100644 index 0000000..bdc95a9 --- /dev/null +++ b/docs/Configuration.md @@ -0,0 +1,216 @@ +[Documentation](Index.md) / Configuration + +# Websocket: Configuration + +The Configuration class is used to configure Client, Server and various worker classes. + +## 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/Index.md b/docs/Index.md index 9a4647e..a526781 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 2dde1d0..177d10a 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -138,61 +138,21 @@ $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 -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 @@ -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 methods that provides information. diff --git a/docs/tmp b/docs/tmp deleted file mode 100644 index 01e16b9..0000000 --- a/docs/tmp +++ /dev/null @@ -1,515 +0,0 @@ -abstract class WebSocket\Exception\Exception extends RuntimeException imlements WebSocket\Exception\ExceptionInterface -{ -} - -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; -} - -class WebSocket\Client imlements WebSocket\Runtime\IdentityInterface, Psr\Log\LoggerAwareInterface, Stringable -{ - use WebSocket\Trait\ListenerTrait; - use WebSocket\Trait\LoggerAwareTrait; - use WebSocket\Trait\SendMethodsTrait; - use WebSocket\Trait\StringableTrait; - - public method __construct(Psr\Http\Message\UriInterface|string $uri); - 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 getIdentity(): string; - public method getMeta(string $key): mixed; - 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|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; - 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; -} - -class WebSocket\Connection imlements WebSocket\Runtime\IdentityInterface, Psr\Log\LoggerAwareInterface, Stringable -{ - use WebSocket\Trait\LoggerAwareTrait; - 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); - public method __destruct(); - 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 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|null; - public method getRemoteName(): string|null; - public method getTimeout(): int|float; - 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 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; -} - -class WebSocket\Exception\BadOpcodeException extends WebSocket\Exception\Exception imlements WebSocket\Exception\MessageLevelInterface -{ - public method __construct(string $message = "Bad Opcode"); -} - -class WebSocket\Exception\BadUriException extends WebSocket\Exception\Exception -{ - public method __construct(string $message = "Bad URI"); -} - -class WebSocket\Exception\ClientException extends WebSocket\Exception\Exception -{ -} - -class WebSocket\Exception\CloseException extends WebSocket\Exception\Exception -{ - public method __construct(int|null $status = null, string $content = ""); - public method getCloseStatus(): int; -} - -class WebSocket\Exception\ConnectionClosedException extends WebSocket\Exception\Exception imlements WebSocket\Exception\ConnectionLevelInterface -{ - public method __construct(); -} - -class WebSocket\Exception\ConnectionFailureException extends WebSocket\Exception\Exception imlements WebSocket\Exception\ConnectionLevelInterface -{ - public method __construct(string|null $message = null); -} - -class WebSocket\Exception\ConnectionTimeoutException extends WebSocket\Exception\Exception imlements WebSocket\Exception\MessageLevelInterface -{ - public method __construct(); -} - -class WebSocket\Exception\HandshakeException extends WebSocket\Exception\Exception imlements WebSocket\Exception\ConnectionLevelInterface -{ - public method __construct(string $message, Psr\Http\Message\ResponseInterface $response); - public method getResponse(): Psr\Http\Message\ResponseInterface; -} - -class WebSocket\Exception\ReconnectException extends WebSocket\Exception\Exception -{ - public method __construct(Phrity\Net\Uri|null $uri = null); - public method getUri(): Phrity\Net\Uri|null; -} - -class WebSocket\Exception\ServerException extends WebSocket\Exception\Exception -{ -} - -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; -} - -class WebSocket\Frame\FrameHandler imlements Psr\Log\LoggerAwareInterface, Stringable -{ - use WebSocket\Trait\LoggerAwareTrait; - use WebSocket\Trait\OpcodeTrait; - use WebSocket\Trait\StringableTrait; - - public method __construct(Phrity\Net\SocketStream $stream, bool $pushMasked, bool $pullMaskedRequired); - public method pull(): WebSocket\Frame\Frame; - public method push(WebSocket\Frame\Frame $frame): int; -} - -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; -} - -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 Nyholm\Psr7\Request imlements Psr\Http\Message\RequestInterface -{ - public method __construct(string $method = "GET", Psr\Http\Message\UriInterface|string $uri = ""); -} - -class WebSocket\Http\Response extends Nyholm\Psr7\Response imlements Psr\Http\Message\ResponseInterface -{ - public method __construct(int $code = 200, string $reasonPhrase = ""); -} - -class WebSocket\Http\ServerRequest extends Nyholm\Psr7\ServerRequest imlements Psr\Http\Message\ServerRequestInterface -{ - public method __construct(string $method = "GET", Psr\Http\Message\UriInterface|string $uri = ""); -} - -class WebSocket\Message\Binary extends WebSocket\Message\Message -{ - public method isCompressed(): bool; - public method setCompress(bool $compress): void; -} - -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; -} - -class WebSocket\Message\MessageHandler imlements Psr\Log\LoggerAwareInterface, Stringable -{ - use WebSocket\Trait\LoggerAwareTrait; - use WebSocket\Trait\StringableTrait; - - public method __construct(WebSocket\Frame\FrameHandler $frameHandler); - 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 -{ -} - -class WebSocket\Message\Pong extends WebSocket\Message\Message -{ -} - -class WebSocket\Message\Text extends WebSocket\Message\Message -{ - public method isCompressed(): bool; - public method setCompress(bool $compress): void; -} - -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\LoggerAwareTrait; - 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; -} - -class WebSocket\Middleware\CloseHandler imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, Stringable -{ - use WebSocket\Trait\LoggerAwareTrait; - use WebSocket\Trait\StringableTrait; - - 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; -} - -class WebSocket\Middleware\CompressionExtension imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, WebSocket\Middleware\ProcessIncomingInterface, WebSocket\Middleware\ProcessOutgoingInterface, Stringable -{ - use WebSocket\Trait\LoggerAwareTrait; - 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; -} - -class WebSocket\Middleware\CompressionExtension\DeflateCompressor imlements WebSocket\Middleware\CompressionExtension\CompressorInterface, Stringable -{ - 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; -} - -class WebSocket\Middleware\FollowRedirect imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, Stringable -{ - use WebSocket\Trait\LoggerAwareTrait; - use WebSocket\Trait\StringableTrait; - - public method __construct(int $limit = 10); - public method processHttpIncoming(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection): Psr\Http\Message\MessageInterface; -} - -class WebSocket\Middleware\MiddlewareHandler imlements Psr\Log\LoggerAwareInterface, Stringable -{ - use WebSocket\Trait\LoggerAwareTrait; - use WebSocket\Trait\StringableTrait; - - public method __construct(WebSocket\Message\MessageHandler $messageHandler, WebSocket\Http\HttpHandler $httpHandler); - 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; - public method setLogger(Psr\Log\LoggerInterface $logger): void; -} - -class WebSocket\Middleware\PingInterval imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessOutgoingInterface, WebSocket\Middleware\ProcessTickInterface, Stringable -{ - use WebSocket\Trait\LoggerAwareTrait; - use WebSocket\Trait\StringableTrait; - - 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 Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessIncomingInterface, Stringable -{ - use WebSocket\Trait\LoggerAwareTrait; - use WebSocket\Trait\StringableTrait; - - public method __construct(); - public method processIncoming(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection): WebSocket\Message\Message; -} - -class WebSocket\Middleware\ProcessHttpStack imlements Stringable -{ - use WebSocket\Trait\StringableTrait; - - 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 -{ - 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; -} - -class WebSocket\Middleware\ProcessTickStack imlements Stringable -{ - use WebSocket\Trait\StringableTrait; - - public method __construct(WebSocket\Connection $connection, array $processors); - public method handleTick(): void; -} - -class WebSocket\Middleware\SubprotocolNegotiation imlements Psr\Log\LoggerAwareInterface, WebSocket\Middleware\ProcessHttpOutgoingInterface, WebSocket\Middleware\ProcessHttpIncomingInterface, Stringable -{ - use WebSocket\Trait\LoggerAwareTrait; - 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; -} - -class WebSocket\Server imlements WebSocket\Runtime\IdentityInterface, Psr\Log\LoggerAwareInterface, Stringable -{ - use WebSocket\Trait\ListenerTrait; - use WebSocket\Trait\LoggerAwareTrait; - use WebSocket\Trait\SendMethodsTrait; - use WebSocket\Trait\StringableTrait; - - public method __construct(int $port = 80, bool $ssl = false); - 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 getIdentity(): string; - 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|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; - 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; -} - -inteface WebSocket\Constant -{ - public const GUID; -} - -inteface WebSocket\Exception\ConnectionLevelInterface imlements WebSocket\Exception\ExceptionInterface -{ -} - -inteface WebSocket\Exception\ExceptionInterface imlements Throwable -{ - public method getMessage(): string; -} - -inteface WebSocket\Exception\MessageLevelInterface imlements WebSocket\Exception\ExceptionInterface -{ -} - -inteface WebSocket\Middleware\CompressionExtension\CompressorInterface imlements Stringable -{ - 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; -} - -inteface WebSocket\Middleware\MiddlewareInterface imlements Stringable -{ -} - -inteface WebSocket\Middleware\ProcessHttpIncomingInterface imlements WebSocket\Middleware\MiddlewareInterface -{ - public method processHttpIncoming(WebSocket\Middleware\ProcessHttpStack $stack, WebSocket\Connection $connection): Psr\Http\Message\MessageInterface; -} - -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; -} - -inteface WebSocket\Middleware\ProcessIncomingInterface imlements WebSocket\Middleware\MiddlewareInterface -{ - public method processIncoming(WebSocket\Middleware\ProcessStack $stack, WebSocket\Connection $connection): WebSocket\Message\Message; -} - -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; -} - -inteface WebSocket\Middleware\ProcessTickInterface imlements WebSocket\Middleware\MiddlewareInterface -{ - public method processTick(WebSocket\Middleware\ProcessTickStack $stack, WebSocket\Connection $connection): void; -} - -inteface WebSocket\Runtime\IdentityInterface -{ - public method getIdentity(): string; -} - -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\LoggerAwareTrait -{ - public method attachLogger(mixed $instance): void; - public method initLogger(Psr\Log\LoggerInterface|null $logger = null): void; - public method setLogger(Psr\Log\LoggerInterface $logger): void; -} - -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; -} diff --git a/docu.json b/docu.json index 4331769..0968052 100644 --- a/docu.json +++ b/docu.json @@ -13,10 +13,14 @@ { "name": "Server", "source": "docs/Server.md" - }, + } ], "name": "Resources", "content": [ + { + "name": "Configuration", + "source": "docs/Configuration.md" + }, { "name": "Connection", "source": "docs/Connection.md" @@ -64,9 +68,9 @@ { "name": "Creating your own middleware", "source": "docs/Middleware/Creating.md" - }, + } ] - }, + } ], "name": "Developer resources", "content": [ @@ -85,14 +89,18 @@ { "name": "Class synopsis", "source": "docs/Class_Synopsis.md" - }, + } ], "name": "Migration", "content": [ + { + "name": "Migration v1 -> v2", + "source": "docs/Migrate_1_2.md" + }, { "name": "Migration v2 -> v3", "source": "docs/Migrate_2_3.md" - }, + } ] }, { diff --git a/examples/delegating_server.php b/examples/delegating_server.php index 6c78657..e044fc8 100644 --- a/examples/delegating_server.php +++ b/examples/delegating_server.php @@ -54,7 +54,7 @@ * ssl: bool, * timeout: int<0, max>|float, * framesize: int<1, max>, - * connections: int<0, max>|null, + * connections: int<1, max>|null, * deflate: bool, * } $options */ @@ -69,42 +69,41 @@ } try { + // Configuration + $configuration = new Configuration(); + $configuration->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->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 a718d85..3ddcd94 100644 --- a/examples/echoserver.php +++ b/examples/echoserver.php @@ -53,7 +53,7 @@ * ssl: bool, * timeout: int<0, max>|float, * framesize: int<1, max>, - * connections: int<0, max>|null, + * connections: int<1, max>|null, * deflate: bool, * } $options */ @@ -63,29 +63,31 @@ // Initiate server. try { - $server = new Server($options['port'], isset($options['ssl'])); - $server - ->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->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"; @@ -121,8 +123,8 @@ $msg .= " - Connected: " . json_encode($connection->isConnected()) . "\n"; $msg .= " - Readable: " . json_encode($connection->isReadable()) . "\n"; $msg .= " - Writable: " . json_encode($connection->isWritable()) . "\n"; - $msg .= " - Timeout: {$connection->getTimeout()}s\n"; - $msg .= " - Frame size: {$connection->getFrameSize()}b\n"; + $msg .= " - Timeout: {$connection->getConfiguration()->getTimeout()}s\n"; + $msg .= " - Frame size: {$connection->getConfiguration()->getFrameSize()}b\n"; echo "< [{$connection->getRemoteName()}] {$msg}"; $server->send(new Text($msg)); break; diff --git a/examples/random_client.php b/examples/random_client.php index e448501..1b97d1b 100644 --- a/examples/random_client.php +++ b/examples/random_client.php @@ -77,31 +77,32 @@ ], getopt('', ['uri:', 'timeout:', 'framesize:', 'deflate'])); try { - $client = new Client($options['uri']); - $client - ->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 abc721c..8532f00 100644 --- a/examples/random_server.php +++ b/examples/random_server.php @@ -63,7 +63,7 @@ * ssl: bool, * timeout: int<0, max>|float, * framesize: int<1, max>, - * connections: int<0, max>|null, + * connections: int<1, max>|null, * deflate: bool, * } $options */ @@ -75,29 +75,31 @@ // Initiate server. try { - $server = new Server($options['port'], isset($options['ssl'])); - $server - ->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->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..4409f74 100644 --- a/examples/send.php +++ b/examples/send.php @@ -57,7 +57,22 @@ // Initiate client. try { - $client = new Client($options['uri']); + // Configuration + $configuration = new Configuration(); + if (class_exists(ConsoleLogger::class)) { + $configuration->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/Client.php b/src/Client.php index cddbeb6..605b469 100644 --- a/src/Client.php +++ b/src/Client.php @@ -41,8 +41,8 @@ use WebSocket\Middleware\MiddlewareInterface; use WebSocket\Runtime\IdentityInterface; use WebSocket\Trait\{ + ConfigurationTrait, ListenerTrait, - LoggerAwareTrait, SendMethodsTrait, StringableTrait }; @@ -53,20 +53,16 @@ */ class Client implements IdentityInterface, LoggerAwareInterface, Stringable { + use ConfigurationTrait; /** @use ListenerTrait */ use ListenerTrait; - use LoggerAwareTrait; use SendMethodsTrait; use StringableTrait; + private const SCOPE = 'client'; + // Settings - /** @var int<0, max>|float $timeout */ - private int|float $timeout = 60; - /** @var int<1, max> $frameSize */ - private int $frameSize = 4096; - private bool $persistent = false; - private Context $context; - /** @var array $headers */ + /** @var array $headers */ private array $headers = []; // Internal resources @@ -86,15 +82,15 @@ class Client implements IdentityInterface, LoggerAwareInterface, Stringable /** * @param UriInterface|string $uri A ws/wss-URI + * @param Configuration|null $configuration */ - public function __construct(UriInterface|string $uri) + public function __construct(UriInterface|string $uri, Configuration|null $configuration = null) { $this->socketUri = $this->parseUri($uri); - $this->initLogger(); - $this->context = new Context(); $this->setStreamFactory(new StreamFactory()); $this->httpFactory = new DefaultHttpFactory(); $this->identity = "client/{$this->socketUri->getHost()}"; + $this->initConfiguration($configuration); } /** @@ -139,12 +135,13 @@ public function setHttpFactory(HttpFactory $httpFactory): self /** * 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->logger = $logger; + $this->configuration->setLogger($logger); if ($this->connection) { - $this->connection->setLogger($this->logger); + $this->connection->setLogger($logger); } } @@ -153,13 +150,11 @@ public function setLogger(LoggerInterface $logger): void * @param int<0, max>|float $timeout Timeout in seconds * @return self * @throws InvalidArgumentException If invalid timeout provided + * @deprecated Will be removed in future version, set on Configuration instead */ public function setTimeout(int|float $timeout): self { - if ($timeout < 0) { - throw new InvalidArgumentException("Invalid timeout '{$timeout}' provided"); - } - $this->timeout = $timeout; + $this->configuration->setTimeout($timeout); if ($this->connection) { $this->connection->setTimeout($timeout); } @@ -169,10 +164,11 @@ public function setTimeout(int|float $timeout): self /** * Get timeout. * @return int<0, max>|float Timeout in seconds + * @deprecated Will be removed in future version, get from Configuration instead */ public function getTimeout(): int|float { - return $this->timeout; + return $this->configuration->getTimeout(); } /** @@ -180,13 +176,11 @@ public function getTimeout(): int|float * @param int<1, max> $frameSize Max frame payload size in bytes * @return self * @throws InvalidArgumentException If invalid frameSize provided + * @deprecated Will be removed in future version, set on Configuration instead */ public function setFrameSize(int $frameSize): self { - if ($frameSize < 1) { - throw new InvalidArgumentException("Invalid frameSize '{$frameSize}' provided"); - } - $this->frameSize = $frameSize; + $this->configuration->setFrameSize($frameSize); if ($this->connection) { $this->connection->setFrameSize($frameSize); } @@ -196,20 +190,22 @@ public function setFrameSize(int $frameSize): self /** * Get frame size. * @return int Frame size in bytes + * @deprecated Will be removed in future version, get from Configuration instead */ public function getFrameSize(): int { - return $this->frameSize; + return $this->configuration->getFrameSize(); } /** * Set connection persistence. * @param bool $persistent True for persistent connection. + * @deprecated Will be removed in future version, set on Configuration instead * @return self */ public function setPersistent(bool $persistent): self { - $this->persistent = $persistent; + $this->configuration->setPersistent($persistent); return $this; } @@ -218,13 +214,14 @@ public function setPersistent(bool $persistent): self * @param Context|array $context Context or options as array * @see https://www.php.net/manual/en/context.php * @return self + * @deprecated Will be removed in future version, set on Configuration instead */ public function setContext(Context|array $context): self { if ($context instanceof Context) { - $this->context = $context; + $this->configuration->setContext($context); } else { - $this->context->setOptions($context); + $this->configuration->getContext()->setOptions($context); trigger_error('Calling Client.setContext with array is deprecated, use Context class.', E_USER_DEPRECATED); } return $this; @@ -233,10 +230,11 @@ public function setContext(Context|array $context): self /** * Get current stream context. * @return Context + * @deprecated Will be removed in future version, get from Configuration instead */ public function getContext(): Context { - return $this->context; + return $this->configuration->getContext(); } /** @@ -301,12 +299,18 @@ 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("[{scope}] Client is already running", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + ]); return; } $this->running = true; $reconnect = false; - $this->logger->info("[client] Client is running"); + $this->configuration->getLogger()->info("[{scope}] Client is running", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + ]); $connection = $this->connection(); @@ -316,7 +320,7 @@ public function start(int|float|null $timeout = null): void $streams = $this->streams; try { // Get streams with readable content - $readables = $streams->waitRead($timeout ?? $this->timeout); + $readables = $streams->waitRead($timeout ?? $this->configuration->getTimeout()); foreach ($readables as $key => $readable) { try { // Read from connection @@ -324,12 +328,24 @@ 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("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); $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("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); $this->dispatch('error', [$this, $connection, $e]); } } @@ -341,7 +357,13 @@ 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("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); $this->dispatch('error', [$this, $connection, $e]); } catch (ReconnectException $e) { // Reconnect connection @@ -350,21 +372,39 @@ 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("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); $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("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); $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("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); throw $e; } gc_collect_cycles(); // Collect garbage @@ -382,7 +422,10 @@ 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("[{scope}] Client is stopped", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + ]); } /** @@ -446,13 +489,18 @@ public function connect(): void $stream = null; try { - $client = $this->streamFactory->createSocketClient($hostUri, $this->context); - $client->setPersistent($this->persistent); - $client->setTimeout($this->timeout); + $client = $this->streamFactory->createSocketClient($hostUri, $this->configuration->getContext()); + $client->setPersistent($this->configuration->isPersistent()); + $client->setTimeout($this->configuration->getTimeout()); $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("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); throw new ClientException($error); } $this->connection = new Connection( @@ -460,36 +508,49 @@ public function connect(): void true, false, $hostUri->getScheme() === 'ssl', - $this->httpFactory + $this->httpFactory, + clone $this->configuration ); $this->streams->attach($stream, $this->connection->getIdentity()); - $this->connection->setFrameSize($this->frameSize); - $this->connection->setTimeout($this->timeout); - $this->connection->setLogger($this->logger); foreach ($this->middlewares as $middleware) { $this->connection->addMiddleware($middleware); } if (!$this->isConnected()) { - $error = "Invalid stream on \"{$hostUri}\"."; - $this->logger->error("[client] {$error}"); - throw new ClientException($error); + $this->configuration->getLogger()->error("[{scope}] Invalid stream on {uri}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $this->connection->getIdentity(), + 'uri' => $hostUri, + ]); + throw new ClientException("Invalid stream on \"{$hostUri}\"."); } try { - if (!$this->persistent || $stream->tell() == 0) { + if (!$this->configuration->isPersistent() || $stream->tell() == 0) { /** @throws ReconnectException */ $response = $this->performHandshake($this->socketUri, $this->connection); } } catch (ReconnectException $e) { - $this->logger->info("[client] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->info("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $this->connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); if ($uri = $e->getUri()) { $this->socketUri = $uri; } $this->connect(); return; } - $this->logger->info("[client] Client connected to {$this->socketUri}"); + $this->configuration->getLogger()->info("[{scope}] Client connected to {uri}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $this->connection->getIdentity(), + 'uri' => $this->socketUri, + ]); $this->dispatch('handshake', [ $this, $this->connection, @@ -506,7 +567,11 @@ public function disconnect(): void { if ($this->connection && $this->isConnected()) { $this->connection->disconnect(); - $this->logger->info('[client] Client disconnected'); + $this->configuration->getLogger()->info("[{scope}] Client disconnected", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $this->connection->getIdentity(), + ]); $this->dispatch('disconnect', [$this, $this->connection]); } } @@ -609,11 +674,23 @@ 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("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); throw $e; } - $this->logger->debug("[client] Handshake on {$uri->getPath()}"); + $this->configuration->getLogger()->debug("[{scope}] Handshake on {path}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'path' => $uri->getPath(), + ]); + $connection->setHandshakeRequest($request); $connection->setHandshakeResponse($response); diff --git a/src/Configuration.php b/src/Configuration.php new file mode 100644 index 0000000..bff4e36 --- /dev/null +++ b/src/Configuration.php @@ -0,0 +1,195 @@ +|float $timeout */ + private int|float $timeout; + /** @var int<1, max> $frameSize */ + private int $frameSize; + private bool $persistent; + /** @var int<1, max>|null $maxConnections */ + private int|null $maxConnections; + + + /* ---------- Magic methods ------------------------------------------------------------------------------------ */ + + /** + * @param LoggerInterface|null $logger + * @param Context|null $context + * @param int<0, max>|float $timeout + * @param int<1, max> $frameSize + * @param bool $persistent + * @param int<1, max>|null $maxConnections + */ + public function __construct( + LoggerInterface|null $logger = null, + Context|null $context = null, + int|float|null $timeout = null, + int|null $frameSize = null, + bool|null $persistent = null, + int|null $maxConnections = null, + ) { + $this->setLogger($logger ?? new NullLogger()); + $this->setContext($context ?? new Context()); + $this->setTimeout($timeout ?? 60); + $this->setFrameSize($frameSize ?? 4096); + $this->setPersistent($persistent ?? false); + $this->setMaxConnections($maxConnections); + } + + public function __toString(): string + { + return $this->stringable(''); + } + + + /* ---------- Logger methods ----------------------------------------------------------------------------------- */ + + /** + * @return LoggerInterface $logger + */ + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + /** + * @param LoggerInterface $logger + */ + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } + + + /* ---------- Context methods ---------------------------------------------------------------------------------- */ + + /** + * @return Context + */ + public function getContext(): Context + { + return $this->context; + } + + /** + * @param Context $context Context or options as array + */ + public function setContext(Context $context): void + { + $this->context = $context; + } + + + /* ---------- Timeout methods ---------------------------------------------------------------------------------- */ + + /** + * @return int<0, max>|float Timeout in seconds + */ + public function getTimeout(): int|float + { + return $this->timeout; + } + + /** + * @param int<0, max>|float $timeout Timeout in seconds + * @throws InvalidArgumentException If invalid timeout provided + */ + public function setTimeout(int|float $timeout): void + { + if ($timeout < 0) { + throw new InvalidArgumentException("Invalid timeout '{$timeout}' provided"); + } + $this->timeout = $timeout; + } + + + /* ---------- FrameSize methods -------------------------------------------------------------------------------- */ + + /** + * @return int<1, max> Frame size in bytes + */ + public function getFrameSize(): int + { + return $this->frameSize; + } + + /** + * @param int<1, max> $frameSize Max frame payload size in bytes + * @throws InvalidArgumentException If invalid frameSize provided + */ + public function setFrameSize(int $frameSize): void + { + if ($frameSize < 1) { + throw new InvalidArgumentException("Invalid frameSize '{$frameSize}' provided"); + } + $this->frameSize = $frameSize; + } + + + /* ---------- Persistent methods (Client only) ----------------------------------------------------------------- */ + + /** + * @return bool $persistent + */ + public function isPersistent(): bool + { + return $this->persistent; + } + + /** + * @param bool $persistent True for persistent connection + */ + public function setPersistent(bool $persistent): void + { + $this->persistent = $persistent; + } + + + /* ---------- Max connections (Server only) -------------------------------------------------------------------- */ + + /** + * @return int<1, max> Max connections allowed + */ + public function getMaxConnections(): int|null + { + return $this->maxConnections; + } + + /** + * @param int<1, max>|null $maxConnections + * @throws InvalidArgumentException If invalid number provided + */ + public function setMaxConnections(int|null $maxConnections): void + { + if ($maxConnections !== null && $maxConnections < 1) { + throw new InvalidArgumentException("Invalid maxConnections '{$maxConnections}' provided"); + } + $this->maxConnections = $maxConnections; + } +} diff --git a/src/Connection.php b/src/Connection.php index 05d54e0..9524a9c 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -46,7 +46,7 @@ }; use WebSocket\Runtime\IdentityInterface; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, SendMethodsTrait, StringableTrait }; @@ -57,18 +57,16 @@ */ class Connection implements IdentityInterface, LoggerAwareInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use SendMethodsTrait; use StringableTrait; + private const SCOPE = 'connection'; + private SocketStream $stream; private HttpHandler $httpHandler; private MessageHandler $messageHandler; private MiddlewareHandler $middlewareHandler; - /** @var int<1, max> $frameSize */ - private int $frameSize = 4096; - /** @var int<0, max>|float $timeout */ - private int|float $timeout = 60; private string $localName; private string $remoteName; private RequestInterface|null $handshakeRequest = null; @@ -87,7 +85,8 @@ public function __construct( bool $pushMasked, bool $pullMaskedRequired, bool $ssl = false, - HttpFactory|null $httpFactory = null + HttpFactory|null $httpFactory = null, + Configuration|null $configuration = null, ) { $this->stream = $stream; $this->httpHandler = new HttpHandler($this->stream, $ssl, $httpFactory); @@ -100,7 +99,8 @@ public function __construct( $this->getIdentityPart($this->localName), $this->getIdentityPart($this->remoteName), ); - $this->initLogger(); + $this->initConfiguration($configuration); + $this->stream->setTimeout($this->configuration->getTimeout()); } public function __destruct() @@ -126,13 +126,18 @@ public function getIdentity(): string /** * 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->logger = $logger; + $this->configuration->setLogger($logger); $this->messageHandler->setLogger($logger); $this->middlewareHandler->setLogger($logger); - $this->logger->debug("[connection] Setting logger: " . get_class($logger)); + $this->configuration->getLogger()->debug("[{scope}] Setting logger: {logger}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'logger' => get_class($logger), + ]); } /** @@ -140,25 +145,28 @@ public function setLogger(LoggerInterface $logger): void * @param int<0, max>|float $timeout Timeout part in seconds * @return self * @throws InvalidArgumentException + * @deprecated Will be removed in future version, set on Configuration instead */ public function setTimeout(int|float $timeout): self { - if ($timeout < 0) { - throw new InvalidArgumentException("Invalid timeout '{$timeout}' provided"); - } - $this->timeout = $timeout; + $this->configuration->setTimeout($timeout); $this->stream->setTimeout($timeout); - $this->logger->debug("[connection] Setting timeout: {$timeout} seconds"); + $this->configuration->getLogger()->debug("[{scope}] Setting timeout: {timeout} seconds", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'timeout' => $timeout, + ]); return $this; } /** * Get timeout. * @return int<0, max>|float Timeout in seconds. + * @deprecated Will be removed in future version, get from Configuration instead */ public function getTimeout(): int|float { - return $this->timeout; + return $this->configuration->getTimeout(); } /** @@ -166,23 +174,22 @@ public function getTimeout(): int|float * @param int<1, max> $frameSize Frame size in bytes. * @return self * @throws InvalidArgumentException + * @deprecated Will be removed in future version, set on Configuration instead */ public function setFrameSize(int $frameSize): self { - if ($frameSize < 1) { - throw new InvalidArgumentException("Invalid frameSize '{$frameSize}' provided"); - } - $this->frameSize = $frameSize; + $this->configuration->setFrameSize($frameSize); return $this; } /** * Get frame size. * @return int<1, max> Frame size in bytes + * @deprecated Will be removed in future version, get from Configuration instead */ public function getFrameSize(): int { - return max(1, $this->frameSize); + return $this->configuration->getFrameSize(); } /** @@ -202,7 +209,11 @@ 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("[{scope}] Added middleware: {middleware}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'middleware' => $middleware, + ]); return $this; } @@ -242,7 +253,10 @@ public function isWritable(): bool */ public function disconnect(): self { - $this->logger->info('[connection] Closing connection'); + $this->configuration->getLogger()->info("[{scope}] Closing connection", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + ]); $this->stream->close(); $this->closed = true; return $this; @@ -254,7 +268,10 @@ public function disconnect(): self */ public function closeRead(): self { - $this->logger->info('[connection] Closing further reading'); + $this->configuration->getLogger()->info("[{scope}] Closing further reading", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + ]); $this->stream->closeRead(); return $this; } @@ -265,7 +282,10 @@ public function closeRead(): self */ public function closeWrite(): self { - $this->logger->info('[connection] Closing further writing'); + $this->configuration->getLogger()->info("[{scope}] Closing further writing", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + ]); $this->stream->closeWrite(); return $this; } @@ -422,11 +442,21 @@ 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("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); throw $e; } if ($e instanceof ExceptionInterface) { - $this->logger->error("[connection] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); throw $e; } // External exceptions are converted to internal @@ -434,15 +464,32 @@ 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("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + 'meta' => $meta + ]); throw new ConnectionTimeoutException(); } if (!empty($meta['eof'])) { - $this->logger->error("[connection] {$e->getMessage()}", ['exception' => $e, 'meta' => $meta]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + 'meta' => $meta + ]); throw new ConnectionClosedException(); } } - $this->logger->error("[connection] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); throw new ConnectionFailureException(); } diff --git a/src/Frame/FrameHandler.php b/src/Frame/FrameHandler.php index 755dc27..9ff8700 100644 --- a/src/Frame/FrameHandler.php +++ b/src/Frame/FrameHandler.php @@ -8,12 +8,16 @@ namespace WebSocket\Frame; use Phrity\Net\SocketStream; -use Psr\Log\LoggerAwareInterface; +use Psr\Log\{ + LoggerAwareInterface, + LoggerInterface, +}; use RuntimeException; use Stringable; +use WebSocket\Configuration; use WebSocket\Exception\CloseException; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, OpcodeTrait, StringableTrait }; @@ -24,20 +28,36 @@ */ class FrameHandler implements LoggerAwareInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use OpcodeTrait; use StringableTrait; + private const SCOPE = 'frame-handler'; + private SocketStream $stream; private bool $pushMasked; private bool $pullMaskedRequired; - public function __construct(SocketStream $stream, bool $pushMasked, bool $pullMaskedRequired) - { + public function __construct( + SocketStream $stream, + bool $pushMasked, + bool $pullMaskedRequired, + Configuration|null $configuration = null, + ) { $this->stream = $stream; $this->pushMasked = $pushMasked; $this->pullMaskedRequired = $pullMaskedRequired; - $this->initLogger(); + $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); } /** @@ -96,14 +116,17 @@ public function pull(): Frame } $frame = new Frame($opcode, $payload, $final, $rsv1, $rsv2, $rsv3); - $this->logger->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->logger->error("[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'); } @@ -158,7 +181,8 @@ public function push(Frame $frame): int // Write to stream. $written = $this->write($data); - $this->logger->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/Message/MessageHandler.php b/src/Message/MessageHandler.php index 87be81c..03432cf 100644 --- a/src/Message/MessageHandler.php +++ b/src/Message/MessageHandler.php @@ -12,13 +12,14 @@ LoggerInterface, }; use Stringable; +use WebSocket\Configuration; use WebSocket\Exception\BadOpcodeException; use WebSocket\Frame\{ Frame, FrameHandler, }; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -28,24 +29,30 @@ */ class MessageHandler implements LoggerAwareInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; private const DEFAULT_SIZE = 4096; + private const SCOPE = 'message-handler'; private FrameHandler $frameHandler; /** @var array $frameBuffer */ private array $frameBuffer = []; - public function __construct(FrameHandler $frameHandler) + public function __construct(FrameHandler $frameHandler, Configuration|null $configuration = null) { $this->frameHandler = $frameHandler; - $this->initLogger(); + $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->logger = $logger; + $this->configuration->setLogger($logger); $this->frameHandler->setLogger($logger); } @@ -62,7 +69,9 @@ 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("[{scope}] Pushed {message}", [ + 'scope' => self::SCOPE, + 'message' => $message, 'opcode' => $message->getOpcode(), 'content-length' => $message->getLength(), 'frames' => count($frames), @@ -108,7 +117,9 @@ 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("[{scope}] Pulled {message}", [ + 'scope' => self::SCOPE, + 'message' => $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..73cef46 100644 --- a/src/Middleware/Callback.php +++ b/src/Middleware/Callback.php @@ -9,12 +9,18 @@ use Closure; use Psr\Http\Message\MessageInterface; -use Psr\Log\LoggerAwareInterface; +use Psr\Log\{ + LoggerInterface, + LoggerAwareInterface, +}; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Configuration, + Connection, +}; use WebSocket\Message\Message; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -31,7 +37,7 @@ class Callback implements ProcessTickInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; private Closure|null $incoming; @@ -52,7 +58,17 @@ public function __construct( $this->httpIncoming = $httpIncoming; $this->httpOutgoing = $httpOutgoing; $this->tick = $tick; - $this->initLogger(); + $this->initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); } public function processIncoming(ProcessStack $stack, Connection $connection): Message diff --git a/src/Middleware/CloseHandler.php b/src/Middleware/CloseHandler.php index c5eafb5..6875eba 100644 --- a/src/Middleware/CloseHandler.php +++ b/src/Middleware/CloseHandler.php @@ -7,15 +7,21 @@ namespace WebSocket\Middleware; -use Psr\Log\LoggerAwareInterface; +use Psr\Log\{ + LoggerInterface, + LoggerAwareInterface, +}; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Configuration, + Connection, +}; use WebSocket\Message\{ Close, Message }; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -25,12 +31,24 @@ */ class CloseHandler implements LoggerAwareInterface, ProcessIncomingInterface, ProcessOutgoingInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; + private const SCOPE = 'close-handler'; + public function __construct() { - $this->initLogger(); + $this->initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); } public function processIncoming(ProcessStack $stack, Connection $connection): Message @@ -41,13 +59,21 @@ 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("[{scope}] Received 'close', status: {status}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + '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("[{scope}] Received 'close' acknowledge, disconnecting", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'status' => $message->getCloseStatus(), + ]); $connection->disconnect(); } return $message; @@ -61,11 +87,19 @@ 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("[{scope}] Sent 'close', status: {status}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'status' => $message->getCloseStatus(), + ]); $connection->closeWrite(); } else { // Local sent Close/Ack: disconnect - $this->logger->debug("[close-handler] Sent 'close' acknowledge, disconnecting"); + $this->configuration->getLogger()->debug("[{scope}] Sent 'close' acknowledge, disconnecting", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'status' => $message->getCloseStatus(), + ]); $connection->disconnect(); } return $message; diff --git a/src/Middleware/CompressionExtension.php b/src/Middleware/CompressionExtension.php index a8f8027..0884f4b 100644 --- a/src/Middleware/CompressionExtension.php +++ b/src/Middleware/CompressionExtension.php @@ -15,9 +15,15 @@ ResponseInterface, ServerRequestInterface, }; -use Psr\Log\LoggerAwareInterface; +use Psr\Log\{ + LoggerInterface, + LoggerAwareInterface, +}; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Configuration, + Connection, +}; use WebSocket\Message\{ Binary, Message, @@ -25,7 +31,7 @@ }; use WebSocket\Middleware\CompressionExtension\CompressorInterface; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -42,16 +48,28 @@ class CompressionExtension implements ProcessOutgoingInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; + private const SCOPE = 'permessage-compression'; + /** @var array $compressors */ private array $compressors = []; public function __construct(CompressorInterface ...$compressors) { $this->compressors = $compressors; - $this->initLogger(); + $this->initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); } public function processHttpOutgoing( @@ -91,20 +109,24 @@ 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( - "[permessage-compression] Using {$preferred->compressor}", - (array)$preferred->configuration - ); + $this->configuration->getLogger()->debug("[{scope}] Using: {compressor}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + '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->logger->debug( - "[permessage-compression] Using {$preferred->compressor}", - (array)$preferred->configuration - ); + $this->configuration->getLogger()->debug("[{scope}] Using: {compressor}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'compressor' => $preferred->compressor, + 'configuration' => (array)$preferred->configuration, + ]); } // @todo: If not found? } diff --git a/src/Middleware/FollowRedirect.php b/src/Middleware/FollowRedirect.php index addc826..4b191e4 100644 --- a/src/Middleware/FollowRedirect.php +++ b/src/Middleware/FollowRedirect.php @@ -14,15 +14,21 @@ MessageInterface, ResponseInterface, }; -use Psr\Log\LoggerAwareInterface; +use Psr\Log\{ + LoggerInterface, + LoggerAwareInterface, +}; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Configuration, + Connection, +}; use WebSocket\Exception\{ HandshakeException, ReconnectException, }; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -32,16 +38,28 @@ */ class FollowRedirect implements LoggerAwareInterface, ProcessHttpIncomingInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; + private const SCOPE = 'follow-redirect'; + private int $limit; private int $attempts = 1; public function __construct(int $limit = 10) { $this->limit = $limit; - $this->initLogger(); + $this->initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); } /** @@ -57,13 +75,20 @@ public function processHttpIncoming(ProcessHttpStack $stack, Connection $connect && $message->getStatusCode() < 400 && $locationHeader = $message->getHeaderLine('Location') ) { - $note = "{$this->attempts} of {$this->limit} redirect attempts"; + $context = [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'attempts' => $this->attempts, + 'limit' => $this->limit, + 'status' => $message->getStatusCode(), + 'location' => $locationHeader, + ]; if ($this->attempts > $this->limit) { - $this->logger->debug("[follow-redirect] Too many redirect attempts, giving up"); + $this->configuration->getLogger()->warning("[{scope}] Too many redirect attempts, giving up", $context); throw new HandshakeException("Too many redirect attempts, giving up", $message); } $this->attempts++; - $this->logger->debug("[follow-redirect] {$message->getStatusCode()} {$locationHeader} ($note)"); + $this->configuration->getLogger()->info("[{scope}] Redirect {status} to {location}", $context); throw new ReconnectException(new Uri($locationHeader)); } return $message; diff --git a/src/Middleware/MiddlewareHandler.php b/src/Middleware/MiddlewareHandler.php index 59ba495..e999d75 100644 --- a/src/Middleware/MiddlewareHandler.php +++ b/src/Middleware/MiddlewareHandler.php @@ -14,14 +14,17 @@ LoggerAwareInterface, }; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Configuration, + Connection, +}; use WebSocket\Http\HttpHandler; use WebSocket\Message\{ Message, MessageHandler }; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -31,9 +34,11 @@ */ class MiddlewareHandler implements LoggerAwareInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; + private const SCOPE = 'middleware-handler'; + // Processor collections /** @var array */ private array $middlewares = []; @@ -57,22 +62,28 @@ class MiddlewareHandler implements LoggerAwareInterface, Stringable * @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(); + $this->initConfiguration($configuration); } /** * Set logger on MiddlewareHandler and all LoggerAware middlewares. * @param LoggerInterface $logger + * @deprecated Will be removed in future version, set on Configuration instead */ public function setLogger(LoggerInterface $logger): void { - $this->logger = $logger; + $this->configuration->setLogger($logger); foreach ($this->middlewares as $middleware) { - $this->attachLogger($middleware); + if ($middleware instanceof LoggerAwareInterface) { + $middleware->setLogger($logger); + } } } @@ -83,27 +94,33 @@ public function setLogger(LoggerInterface $logger): void */ public function add(MiddlewareInterface $middleware): self { + $context = [ + 'scope' => self::SCOPE, + 'middleware' => $middleware, + ]; if ($middleware instanceof ProcessIncomingInterface) { - $this->logger->info("[middleware-handler] Added incoming: {$middleware}"); + $this->configuration->getLogger()->info("[{scope}] Added incoming: {middleware}", $context); $this->incoming[] = $middleware; } if ($middleware instanceof ProcessOutgoingInterface) { - $this->logger->info("[middleware-handler] Added outgoing: {$middleware}"); + $this->configuration->getLogger()->info("[{scope}] Added outgoing: {middleware}", $context); $this->outgoing[] = $middleware; } if ($middleware instanceof ProcessHttpIncomingInterface) { - $this->logger->info("[middleware-handler] Added http incoming: {$middleware}"); + $this->configuration->getLogger()->info("[{scope}] Added http incoming: {middleware}", $context); $this->httpIncoming[] = $middleware; } if ($middleware instanceof ProcessHttpOutgoingInterface) { - $this->logger->info("[middleware-handler] Added http outgoing: {$middleware}"); + $this->configuration->getLogger()->info("[{scope}] Added http outgoing: {middleware}", $context); $this->httpOutgoing[] = $middleware; } if ($middleware instanceof ProcessTickInterface) { - $this->logger->info("[middleware-handler] Added tick: {$middleware}"); + $this->configuration->getLogger()->info("[{scope}] Added tick: {middleware}", $context); $this->tick[] = $middleware; } - $this->attachLogger($middleware); + if ($middleware instanceof LoggerAwareInterface) { + $middleware->setLogger($this->configuration->getLogger()); + } $this->middlewares[] = $middleware; return $this; } @@ -115,7 +132,10 @@ public function add(MiddlewareInterface $middleware): self */ public function processIncoming(Connection $connection): Message { - $this->logger->info("[middleware-handler] Processing incoming"); + $this->configuration->getLogger()->info("[{scope}] Processing incoming", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + ]); $stack = new ProcessStack($connection, $this->messageHandler, $this->incoming); return $stack->handleIncoming(); } @@ -129,7 +149,10 @@ 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("[{scope}] Processing outgoing", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + ]); $stack = new ProcessStack($connection, $this->messageHandler, $this->outgoing); return $stack->handleOutgoing($message); } @@ -141,7 +164,10 @@ 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("[{scope}] Processing http incoming", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + ]); $stack = new ProcessHttpStack($connection, $this->httpHandler, $this->httpIncoming); return $stack->handleHttpIncoming(); } @@ -154,7 +180,10 @@ 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("[{scope}] Processing http outgoing", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + ]); $stack = new ProcessHttpStack($connection, $this->httpHandler, $this->httpOutgoing); return $stack->handleHttpOutgoing($message); } @@ -165,7 +194,10 @@ public function processHttpOutgoing(Connection $connection, MessageInterface $me */ public function processTick(Connection $connection): void { - $this->logger->info("[middleware-handler] Processing tick"); + $this->configuration->getLogger()->info("[{scope}] Processing tick", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + ]); $stack = new ProcessTickStack($connection, $this->tick); $stack->handleTick(); } diff --git a/src/Middleware/PingInterval.php b/src/Middleware/PingInterval.php index 381c054..fb7a6e6 100644 --- a/src/Middleware/PingInterval.php +++ b/src/Middleware/PingInterval.php @@ -7,15 +7,21 @@ namespace WebSocket\Middleware; -use Psr\Log\LoggerAwareInterface; +use Psr\Log\{ + LoggerInterface, + LoggerAwareInterface, +}; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Configuration, + Connection, +}; use WebSocket\Message\{ Ping, Message }; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -25,15 +31,27 @@ */ class PingInterval implements LoggerAwareInterface, ProcessOutgoingInterface, ProcessTickInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; + private const SCOPE = 'ping-interval'; + private int|float|null $interval; public function __construct(int|float|null $interval = null) { $this->interval = $interval; - $this->initLogger(); + $this->initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); } public function processOutgoing(ProcessStack $stack, Connection $connection, Message $message): Message @@ -46,7 +64,10 @@ 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("[{scope}] Auto-pushing ping", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + ]); $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..50ec902 100644 --- a/src/Middleware/PingResponder.php +++ b/src/Middleware/PingResponder.php @@ -7,16 +7,22 @@ namespace WebSocket\Middleware; -use Psr\Log\LoggerAwareInterface; +use Psr\Log\{ + LoggerInterface, + LoggerAwareInterface, +}; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Configuration, + Connection, +}; use WebSocket\Message\{ Ping, Pong, Message }; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -26,12 +32,22 @@ */ class PingResponder implements LoggerAwareInterface, ProcessIncomingInterface, Stringable { + use ConfigurationTrait; use StringableTrait; - use LoggerAwareTrait; public function __construct() { - $this->initLogger(); + $this->initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); } public function processIncoming(ProcessStack $stack, Connection $connection): Message diff --git a/src/Middleware/ProcessStack.php b/src/Middleware/ProcessStack.php index 142e7b7..9acfa4a 100644 --- a/src/Middleware/ProcessStack.php +++ b/src/Middleware/ProcessStack.php @@ -68,6 +68,6 @@ public function handleOutgoing(Message $message): Message if ($processor) { return $processor->processOutgoing($this, $this->connection, $message); } - return $this->messageHandler->push($message, $this->connection->getFrameSize()); + return $this->messageHandler->push($message, $this->connection->getConfiguration()->getFrameSize()); } } diff --git a/src/Middleware/SubprotocolNegotiation.php b/src/Middleware/SubprotocolNegotiation.php index dcaacfe..69d903c 100644 --- a/src/Middleware/SubprotocolNegotiation.php +++ b/src/Middleware/SubprotocolNegotiation.php @@ -15,12 +15,18 @@ ResponseInterface, ServerRequestInterface, }; -use Psr\Log\LoggerAwareInterface; +use Psr\Log\{ + LoggerInterface, + LoggerAwareInterface, +}; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Configuration, + Connection, +}; use WebSocket\Exception\HandshakeException; use WebSocket\Trait\{ - LoggerAwareTrait, + ConfigurationTrait, StringableTrait, }; @@ -34,9 +40,11 @@ class SubprotocolNegotiation implements ProcessHttpIncomingInterface, Stringable { - use LoggerAwareTrait; + use ConfigurationTrait; use StringableTrait; + private const SCOPE = 'subprotocol-negotiation'; + /** @var array $subprotocols */ private array $subprotocols; private bool $require; @@ -46,7 +54,17 @@ public function __construct(array $subprotocols, bool $require = false) { $this->subprotocols = $subprotocols; $this->require = $require; - $this->initLogger(); + $this->initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); } public function processHttpOutgoing( @@ -59,14 +77,22 @@ public function processHttpOutgoing( foreach ($this->subprotocols as $subprotocol) { $message = $message->withAddedHeader('Sec-WebSocket-Protocol', $subprotocol); } - if ($supported = implode(', ', $this->subprotocols)) { - $this->logger->debug("[subprotocol-negotiation] Requested subprotocols: {$supported}"); + if ($requested = implode(', ', $this->subprotocols)) { + $this->configuration->getLogger()->debug("[{scope}] Requested subprotocols: {requested}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'requested' => $requested, + ]); } } 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("[{scope}] Selected subprotocol: {selected}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'selected' => $selected, + ]); } elseif ($this->require) { // No matching subprotocol, fail handshake $message = $message->withStatus(426); @@ -86,10 +112,18 @@ 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("[{scope}] Requested subprotocols: {requested}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'requested' => $requested, + ]); } if ($supported = implode(', ', $this->subprotocols)) { - $this->logger->debug("[subprotocol-negotiation] Supported subprotocols: {$supported}"); + $this->configuration->getLogger()->debug("[{scope}] Supported subprotocols: {supported}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'supported' => $supported, + ]); } foreach ($message->getHeader('Sec-WebSocket-Protocol') as $subprotocol) { if (in_array($subprotocol, $this->subprotocols)) { @@ -101,7 +135,11 @@ 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("[{scope}] Selected subprotocol: {selected}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'selected' => $selected, + ]); } elseif ($this->require) { // No matching subprotocol, close and fail $connection->close(); diff --git a/src/Server.php b/src/Server.php index fe519b6..2e20016 100644 --- a/src/Server.php +++ b/src/Server.php @@ -42,8 +42,8 @@ use WebSocket\Middleware\MiddlewareInterface; use WebSocket\Runtime\IdentityInterface; use WebSocket\Trait\{ + ConfigurationTrait, ListenerTrait, - LoggerAwareTrait, SendMethodsTrait, StringableTrait }; @@ -54,20 +54,17 @@ */ class Server implements IdentityInterface, LoggerAwareInterface, Stringable { + use ConfigurationTrait; /** @use ListenerTrait */ use ListenerTrait; - use LoggerAwareTrait; use SendMethodsTrait; use StringableTrait; + private const SCOPE = 'server'; + // Settings private int $port; private string $scheme; - /** @var int<0, max>|float $timeout */ - private int|float $timeout = 60; - /** @var int<1, max> $frameSize */ - private int $frameSize = 4096; - private Context $context; // Internal resources private StreamFactory $streamFactory; @@ -78,7 +75,7 @@ class Server implements IdentityInterface, LoggerAwareInterface, Stringable private array $connections = []; /** @var array $middlewares */ private array $middlewares = []; - private int|null $maxConnections = null; + private bool $allowConnections = false; private HttpFactory $httpFactory; /** @var non-empty-string $identity */ private string $identity; @@ -89,20 +86,20 @@ class Server implements IdentityInterface, LoggerAwareInterface, Stringable /** * @param int $port Socket port to listen to * @param bool $ssl If SSL should be used + * @param Configuration|null $configuration * @throws InvalidArgumentException If invalid port provided */ - public function __construct(int $port = 80, bool $ssl = false) + public function __construct(int $port = 80, bool $ssl = false, Configuration|null $configuration = null) { if ($port < 0 || $port > 65535) { throw new InvalidArgumentException("Invalid port '{$port}' provided"); } $this->port = $port; $this->scheme = $ssl ? 'ssl' : 'tcp'; - $this->initLogger(); - $this->context = new Context(); $this->httpFactory = new DefaultHttpFactory(); $this->setStreamFactory(new StreamFactory()); $this->identity = "server/{$port}"; + $this->initConfiguration($configuration); } /** @@ -147,12 +144,13 @@ public function setHttpFactory(HttpFactory $httpFactory): self /** * 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->logger = $logger; + $this->configuration->setLogger($logger); foreach ($this->connections as $connection) { - $connection->setLogger($this->logger); + $connection->setLogger($logger); } } @@ -161,13 +159,11 @@ public function setLogger(LoggerInterface $logger): void * @param int<0, max>|float $timeout Timeout in seconds * @return self * @throws InvalidArgumentException If invalid timeout provided + * @deprecated Will be removed in future version, set on Configuration instead */ public function setTimeout(int|float $timeout): self { - if ($timeout < 0) { - throw new InvalidArgumentException("Invalid timeout '{$timeout}' provided"); - } - $this->timeout = $timeout; + $this->configuration->setTimeout($timeout); foreach ($this->connections as $connection) { $connection->setTimeout($timeout); } @@ -177,10 +173,11 @@ public function setTimeout(int|float $timeout): self /** * Get timeout. * @return int<0, max>|float Timeout in seconds + * @deprecated Will be removed in future version, get from Configuration instead */ public function getTimeout(): int|float { - return $this->timeout; + return $this->configuration->getTimeout(); } /** @@ -188,13 +185,11 @@ public function getTimeout(): int|float * @param int<1, max> $frameSize Frame size in bytes * @return self * @throws InvalidArgumentException If invalid frameSize provided + * @deprecated Will be removed in future version, set on Configuration instead */ public function setFrameSize(int $frameSize): self { - if ($frameSize < 3) { - throw new InvalidArgumentException("Invalid frameSize '{$frameSize}' provided"); - } - $this->frameSize = $frameSize; + $this->configuration->setFrameSize($frameSize); foreach ($this->connections as $connection) { $connection->setFrameSize($frameSize); } @@ -204,10 +199,11 @@ public function setFrameSize(int $frameSize): self /** * Get frame size. * @return int Frame size in bytes + * @deprecated Will be removed in future version, get from Configuration instead */ public function getFrameSize(): int { - return $this->frameSize; + return $this->configuration->getFrameSize(); } /** @@ -282,13 +278,14 @@ public function getWritableConnections(): array * @param Context|array $context Context or options as array * @see https://www.php.net/manual/en/context.php * @return self + * @deprecated Will be removed in future version, set on Configuration instead */ public function setContext(Context|array $context): self { if ($context instanceof Context) { - $this->context = $context; + $this->configuration->setContext($context); } else { - $this->context->setOptions($context); + $this->configuration->getContext()->setOptions($context); trigger_error('Calling Server.setContext with array is deprecated, use Context class.', E_USER_DEPRECATED); } return $this; @@ -297,10 +294,11 @@ public function setContext(Context|array $context): self /** * Get current stream context. * @return Context + * @deprecated Will be removed in future version, get from Configuration instead */ public function getContext(): Context { - return $this->context; + return $this->configuration->getContext(); } /** @@ -319,16 +317,14 @@ public function addMiddleware(MiddlewareInterface $middleware): self /** * Set maximum number of connections allowed, null means unlimited. - * @param int|null $maxConnections + * @param int<1, max>|null $maxConnections * @return self * @throws InvalidArgumentException If number provided + * @deprecated Will be removed in future version, set on Configuration instead */ public function setMaxConnections(int|null $maxConnections): self { - if ($maxConnections !== null && $maxConnections < 1) { - throw new InvalidArgumentException("Invalid maxConnections '{$maxConnections}' provided"); - } - $this->maxConnections = $maxConnections; + $this->configuration->setMaxConnections($maxConnections); return $this; } @@ -367,11 +363,17 @@ public function start(int|float|null $timeout = null): void // Check if running if ($this->running) { - $this->logger->warning("[server] Server is already running"); + $this->configuration->getLogger()->warning("[{scope}] Server is already running", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + ]); return; } $this->running = true; - $this->logger->info("[server] Server is running"); + $this->configuration->getLogger()->info("[{scope}] Server is running", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + ]); /** @var StreamCollection */ $streams = $this->streams; @@ -387,7 +389,7 @@ public function start(int|float|null $timeout = null): void } // Get streams with readable content - $readables = $this->streams->waitRead($timeout ?? $this->timeout); + $readables = $this->streams->waitRead($timeout ?? $this->configuration->getTimeout()); foreach ($readables as $key => $readable) { try { $connection = null; @@ -402,7 +404,13 @@ 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("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); $this->dispatch('error', [$this, $connection, $e]); } catch (ConnectionLevelInterface $e) { // Error, disconnect connection @@ -411,14 +419,25 @@ public function start(int|float|null $timeout = null): void unset($this->connections[$key]); $connection->disconnect(); } - $this->logger->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); $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->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); $this->dispatch('error', [$this, $connection, $e]); } } @@ -428,11 +447,21 @@ public function start(int|float|null $timeout = null): void $this->dispatch('tick', [$this]); } catch (ExceptionInterface $e) { // Low-level error - $this->logger->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); $this->dispatch('error', [$this, null, $e]); } catch (Throwable $e) { // Crash it - $this->logger->error("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); $this->disconnect(); throw $e; } @@ -446,7 +475,10 @@ public function start(int|float|null $timeout = null): void public function stop(): void { $this->running = false; - $this->logger->info("[server] Server is stopped"); + $this->configuration->getLogger()->info("[{scope}] Server is stopped", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + ]); } /** @@ -467,24 +499,25 @@ public function isRunning(): bool */ public function shutdown(int $closeStatus = 1001): void { - $this->logger->info('[server] Shutting down'); + $this->configuration->getLogger()->info("[{scope}] Shutting down", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + ]); if ($this->getConnectionCount() == 0) { $this->disconnect(); return; } // Store and reset settings, lock new connections, reset listeners - $max = $this->maxConnections; - $this->maxConnections = 0; + $this->allowConnections = false; $listeners = $this->listeners; $this->listeners = []; // Track disconnects - $this->onDisconnect(function () use ($max, $listeners) { + $this->onDisconnect(function () use ($listeners) { if ($this->getConnectionCount() > 0) { return; } $this->disconnect(); // Restore settings - $this->maxConnections = $max; $this->listeners = $listeners; }); // Close all current connections, listen to acks @@ -507,7 +540,10 @@ public function disconnect(): void $this->server->close(); } $this->server = $this->streams = null; - $this->logger->info('[server] Server disconnected'); + $this->configuration->getLogger()->info("[{scope}] Server disconnected", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + ]); } @@ -518,10 +554,16 @@ 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->server = $this->streamFactory->createSocketServer($uri, $this->configuration->getContext()); $this->streams = $this->streamFactory->createStreamCollection(); $this->streams->attach($this->server, $this->identity); - $this->logger->info("[server] Starting server on {$uri}."); + $this->allowConnections = true; + $this->configuration->getLogger()->info("[server] Starting server on {uri}."); + $this->configuration->getLogger()->info("[{scope}] Server disconnected", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'uri' => $uri, + ]); } catch (Throwable $e) { $error = "Server failed to start: {$e->getMessage()}"; throw new ServerException($error); @@ -531,8 +573,21 @@ protected function createSocketServer(): void // Accept connection on socket server protected function acceptSocket(SocketServer $socket): void { - if (!is_null($this->maxConnections) && $this->getConnectionCount() >= $this->maxConnections) { - $this->logger->warning("[server] Denied connection, reached max {$this->maxConnections}"); + $maxConnections = $this->configuration->getMaxConnections(); + if (!is_null($maxConnections) && $this->getConnectionCount() >= $maxConnections) { + $this->configuration->getLogger()->warning("[{scope}] Denied connection, reached max {maxConnections}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connections' => $this->getConnectionCount(), + 'maxConnections' => $maxConnections, + ]); + return; + } + if (!$this->allowConnections) { + $this->configuration->getLogger()->warning("[{scope}] Denied connection, shutting down", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + ]); return; } try { @@ -543,25 +598,26 @@ protected function acceptSocket(SocketServer $socket): void false, true, $this->isSsl(), - $this->httpFactory + $this->httpFactory, + clone $this->configuration ); $this->streams()->attach($stream, $connection->getIdentity()); } catch (StreamException $e) { throw new ConnectionFailureException("Server failed to accept: {$e->getMessage()}"); } try { - $connection->setLogger($this->logger); - $connection - ->setFrameSize($this->frameSize) - ->setTimeout($this->timeout) - ; foreach ($this->middlewares as $middleware) { $connection->addMiddleware($middleware); } /** @throws StreamException */ $request = $this->performHandshake($connection); $this->connections[$connection->getIdentity()] = $connection; - $this->logger->info("[server] Accepted connection from {$connection->getIdentity()}."); + $this->configuration->getLogger()->info("[{scope}] Accepted connection from {connection}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connection' => $connection->getIdentity(), + ]); + $this->dispatch('handshake', [ $this, $connection, @@ -582,7 +638,11 @@ protected function detachUnconnected(): void if (!$connection->isConnected()) { $this->streams()->detach($key); unset($this->connections[$key]); - $this->logger->info("[server] Disconnected {$key}."); + $this->configuration->getLogger()->info("[{scope}] Disconnected {connection}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connection' => $connection->getIdentity(), + ]); $this->dispatch('disconnect', [$this, $connection]); } } @@ -647,7 +707,13 @@ protected function performHandshake(Connection $connection): ServerRequestInterf ->withHeader('Connection', 'Upgrade') ->withHeader('Sec-WebSocket-Accept', $responseKey); } catch (HandshakeException $e) { - $this->logger->warning("[server] {$e->getMessage()}", ['exception' => $e]); + $this->configuration->getLogger()->warning("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); $response = $e->getResponse(); $exception = $e; } @@ -663,7 +729,13 @@ protected function performHandshake(Connection $connection): ServerRequestInterf throw $exception; } - $this->logger->debug("[server] Handshake on {$request->getUri()->getPath()}"); + $this->configuration->getLogger()->debug("[{scope}] Handshake on {path}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connection' => $connection->getIdentity(), + 'path' => $request->getUri()->getPath(), + ]); + $connection->setHandshakeRequest($request); $connection->setHandshakeResponse($response); 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/src/Trait/LoggerAwareTrait.php b/src/Trait/LoggerAwareTrait.php deleted file mode 100644 index 5890b33..0000000 --- a/src/Trait/LoggerAwareTrait.php +++ /dev/null @@ -1,44 +0,0 @@ -logger = $logger ?? new NullLogger(); - } - - public function setLogger(LoggerInterface $logger): void - { - $this->logger = $logger; - } - - public function attachLogger(mixed $instance): void - { - if ($instance instanceof LoggerAwareInterface) { - $instance->setLogger($this->logger); - } - } -} diff --git a/tests/mock/MockStreamTrait.php b/tests/mock/MockStreamTrait.php index a1f8450..23546f2 100644 --- a/tests/mock/MockStreamTrait.php +++ b/tests/mock/MockStreamTrait.php @@ -95,11 +95,10 @@ private function expectWsClientConnect( $this->expectSocketStreamGetRemoteName()->setReturn(function () use ($host, $port) { return "{$host}:{$port}"; }); - - $this->expectStreamCollectionAttach(); $this->expectSocketStreamSetTimeout()->addAssert(function ($method, $params) use ($timeout) { $this->assertEquals($timeout, $params[0]); }); + $this->expectStreamCollectionAttach(); $this->expectSocketStreamIsConnected(); if ($persistent) { $this->expectSocketStreamTell(); @@ -216,8 +215,8 @@ private function expectWsServerAccept( $this->expectSocketStreamGetRemoteName()->setReturn(function () use ($remote) { return $remote; }); - $this->expectStreamCollectionAttach(); - return $this->expectSocketStreamSetTimeout(); + $this->expectSocketStreamSetTimeout(); + return $this->expectStreamCollectionAttach(); } /** diff --git a/tests/suites/client/ClientErrorTest.php b/tests/suites/client/ClientErrorTest.php index 72db148..f7a02a7 100644 --- a/tests/suites/client/ClientErrorTest.php +++ b/tests/suites/client/ClientErrorTest.php @@ -84,8 +84,8 @@ public function testFailedConnection(): void $this->expectContext(); $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); - $this->expectStreamCollectionAttach(); $this->expectSocketStreamSetTimeout(); + $this->expectStreamCollectionAttach(); $this->expectSocketStreamIsConnected()->setReturn(function () { return false; }); diff --git a/tests/suites/client/ConfigTest.php b/tests/suites/client/ConfigTest.php index bec6674..3e4c43c 100644 --- a/tests/suites/client/ConfigTest.php +++ b/tests/suites/client/ConfigTest.php @@ -28,6 +28,7 @@ use Psr\Log\NullLogger; use WebSocket\{ Client, + Configuration, Connection, }; use WebSocket\Middleware\CloseHandler; @@ -354,4 +355,21 @@ public function testHttpFactories(): void unset($client); } + + public function testConfiguration(): void + { + $logger = new NullLogger(); + $this->expectContext(); + $context = new Context(); + $configuration = new Configuration( + logger: $logger, + context: $context, + timeout: 120, + frameSize: 64, + persistent: true, + ); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->assertInstanceOf(Configuration::class, $client->getConfiguration()); + $this->assertSame($client, $client->setConfiguration($configuration)); + } } diff --git a/tests/suites/configuration/ConfigurationTest.php b/tests/suites/configuration/ConfigurationTest.php new file mode 100644 index 0000000..a92bde3 --- /dev/null +++ b/tests/suites/configuration/ConfigurationTest.php @@ -0,0 +1,108 @@ +assertInstanceOf(NullLogger::class, $configuration->getLogger()); + $this->assertInstanceOf(Context::class, $configuration->getContext()); + $this->assertEquals(60, $configuration->getTimeout()); + $this->assertEquals(4096, $configuration->getFrameSize()); + $this->assertFalse($configuration->isPersistent()); + $this->assertEquals(null, $configuration->getMaxConnections()); + $this->assertEquals('WebSocket\Configuration()', "{$configuration}"); + } + + public function testConstructor(): void + { + $configuration = new Configuration( + logger: new ConsoleLogger(), + context: new Context(), + timeout: 360, + frameSize: 65540, + persistent: true, + maxConnections: 10, + ); + $this->assertInstanceOf(ConsoleLogger::class, $configuration->getLogger()); + $this->assertInstanceOf(Context::class, $configuration->getContext()); + $this->assertEquals(360, $configuration->getTimeout()); + $this->assertEquals(65540, $configuration->getFrameSize()); + $this->assertTrue($configuration->isPersistent()); + $this->assertEquals(10, $configuration->getMaxConnections()); + } + + public function testSetters(): void + { + $configuration = new Configuration(); + $configuration->setLogger(new ConsoleLogger()); + $configuration->setContext(new Context()); + $configuration->setTimeout(360); + $configuration->setFrameSize(65540); + $configuration->setPersistent(true); + $configuration->setMaxConnections(10); + $this->assertInstanceOf(ConsoleLogger::class, $configuration->getLogger()); + $this->assertInstanceOf(Context::class, $configuration->getContext()); + $this->assertEquals(360, $configuration->getTimeout()); + $this->assertEquals(65540, $configuration->getFrameSize()); + $this->assertTrue($configuration->isPersistent()); + $this->assertEquals(10, $configuration->getMaxConnections()); + } + + public function testInvalidTimeout(): void + { + $configuration = new Configuration(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Invalid timeout '-1' provided"); + $configuration->setTimeout(-1); + } + + public function testInvalidFrameSize(): void + { + $configuration = new Configuration(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Invalid frameSize '0' provided"); + // @phpstan-ignore argument.type + $configuration->setFrameSize(0); + } + + public function testInvalidMaxConnextions(): void + { + $configuration = new Configuration(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Invalid maxConnections '0' provided"); + // @phpstan-ignore argument.type + $configuration->setMaxConnections(0); + } +} diff --git a/tests/suites/connection/ConnectionTest.php b/tests/suites/connection/ConnectionTest.php index 79329cf..1cf8153 100644 --- a/tests/suites/connection/ConnectionTest.php +++ b/tests/suites/connection/ConnectionTest.php @@ -20,7 +20,10 @@ use Psr\Http\Message\ResponseInterface; use Psr\Log\NullLogger; use Stringable; -use WebSocket\Connection; +use WebSocket\{ + Configuration, + Connection, +}; use WebSocket\Exception\{ BadOpcodeException, BadUriException, @@ -67,6 +70,7 @@ public function testCreate(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->assertInstanceOf(Connection::class, $connection); $this->assertInstanceOf(Stringable::class, $connection); @@ -130,6 +134,7 @@ public function testDestruct(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamIsConnected(); @@ -152,6 +157,7 @@ public function testHttpMessages(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $request = $this->psrFactory->createRequest('GET', 'ws://test.com/path'); $connection->setHandshakeRequest($request); @@ -194,6 +200,7 @@ public function testWebSocketMessages(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $message = new Text('Test message'); @@ -229,6 +236,7 @@ public function testSendHttpError(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { @@ -254,6 +262,7 @@ public function testPullHttpError(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamReadLine()->setReturn(function () { @@ -279,6 +288,7 @@ public function testSendMessageError(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { @@ -304,6 +314,7 @@ public function testPullMessageError(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamRead()->setReturn(function () { diff --git a/tests/suites/connection/ExceptionTest.php b/tests/suites/connection/ExceptionTest.php index 48a77c9..c758fa6 100644 --- a/tests/suites/connection/ExceptionTest.php +++ b/tests/suites/connection/ExceptionTest.php @@ -56,6 +56,7 @@ public function testBadOpcodeException(): void $stream = new SocketStream($temp); $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { @@ -81,6 +82,7 @@ public function testBadUriException(): void $stream = new SocketStream($temp); $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { @@ -105,6 +107,7 @@ public function testConnectionClosedException(): void $stream = new SocketStream($temp); $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { @@ -129,6 +132,7 @@ public function testConnectionFailureException(): void $stream = new SocketStream($temp); $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { @@ -153,6 +157,7 @@ public function testConnectionTimeoutException(): void $stream = new SocketStream($temp); $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { @@ -177,6 +182,7 @@ public function testGenericTimeoutException(): void $stream = new SocketStream($temp); $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { @@ -205,6 +211,7 @@ public function testGenericEofException(): void $stream = new SocketStream($temp); $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { @@ -233,6 +240,7 @@ public function testGenericUnconnectedException(): void $stream = new SocketStream($temp); $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { @@ -260,6 +268,7 @@ public function testGenericConnectedException(): void $stream = new SocketStream($temp); $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectSocketStreamWrite()->setReturn(function () { @@ -288,6 +297,7 @@ public function testInvalidTimeout(): void $stream = new SocketStream($temp); $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectException(InvalidArgumentException::class); @@ -309,6 +319,7 @@ public function testInvalidFrameSize(): void $stream = new SocketStream($temp); $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $this->expectException(InvalidArgumentException::class); diff --git a/tests/suites/middleware/CallbackTest.php b/tests/suites/middleware/CallbackTest.php index 4418e27..1bfe403 100644 --- a/tests/suites/middleware/CallbackTest.php +++ b/tests/suites/middleware/CallbackTest.php @@ -55,6 +55,7 @@ public function testIncoming(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $middleware = new Callback(incoming: function ($stack, $connection) { $message = $stack->handleIncoming(); @@ -92,6 +93,7 @@ public function testOutgoing(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware(new Callback(outgoing: function ($stack, $connection, $message) { @@ -121,6 +123,7 @@ public function testHttpIncoming(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware(new Callback(httpIncoming: function ($stack, $connection) { @@ -158,6 +161,7 @@ public function testHttpOutgoing(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware(new Callback(httpOutgoing: function ($stack, $connection, $message) { @@ -187,6 +191,7 @@ public function testTick(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware(new Callback(tick: function ($stack, $connection) { diff --git a/tests/suites/middleware/CloseHandlerTest.php b/tests/suites/middleware/CloseHandlerTest.php index 6279f1c..bab9e66 100644 --- a/tests/suites/middleware/CloseHandlerTest.php +++ b/tests/suites/middleware/CloseHandlerTest.php @@ -46,6 +46,7 @@ public function testLocalClose(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $middleware = new CloseHandler(); @@ -81,6 +82,7 @@ public function testRemoteClose(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware(new CloseHandler()); diff --git a/tests/suites/middleware/DeflateCompressorTest.php b/tests/suites/middleware/DeflateCompressorTest.php index 98085a2..0a6a4c5 100644 --- a/tests/suites/middleware/DeflateCompressorTest.php +++ b/tests/suites/middleware/DeflateCompressorTest.php @@ -57,6 +57,7 @@ public function testClientDefault(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $compressor = new DeflateCompressor(); @@ -153,6 +154,7 @@ public function testServerDefault(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $compressor = new DeflateCompressor(); @@ -249,6 +251,7 @@ public function testClientServerDeclines(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $compressor = new DeflateCompressor(); @@ -324,6 +327,7 @@ public function testClientConfiguration(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $compressor = new DeflateCompressor( @@ -425,6 +429,7 @@ public function testServerConfiguration(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $compressor = new DeflateCompressor( @@ -525,6 +530,7 @@ public function testClientConfigurationByServer(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $compressor = new DeflateCompressor( @@ -592,6 +598,7 @@ public function testServerConfigurationByServer(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $compressor = new DeflateCompressor( diff --git a/tests/suites/middleware/FollowRedirectTest.php b/tests/suites/middleware/FollowRedirectTest.php index 1981120..0f92ae9 100644 --- a/tests/suites/middleware/FollowRedirectTest.php +++ b/tests/suites/middleware/FollowRedirectTest.php @@ -59,6 +59,7 @@ public function testRedirect(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware($middleware); @@ -91,6 +92,7 @@ public function testMaxRedirect(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware($middleware); @@ -123,6 +125,7 @@ public function testNoLocation(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware($middleware); diff --git a/tests/suites/middleware/PingIntervalTest.php b/tests/suites/middleware/PingIntervalTest.php index db564ce..66c692e 100644 --- a/tests/suites/middleware/PingIntervalTest.php +++ b/tests/suites/middleware/PingIntervalTest.php @@ -49,6 +49,7 @@ public function testPingInterval(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware($middleware); diff --git a/tests/suites/middleware/PingResponderTest.php b/tests/suites/middleware/PingResponderTest.php index 7e92072..4c287c5 100644 --- a/tests/suites/middleware/PingResponderTest.php +++ b/tests/suites/middleware/PingResponderTest.php @@ -46,6 +46,7 @@ public function testPingAutoResponse(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $middleware = new PingResponder(); diff --git a/tests/suites/middleware/ProcessStackTest.php b/tests/suites/middleware/ProcessStackTest.php index f26381c..12cd0b0 100644 --- a/tests/suites/middleware/ProcessStackTest.php +++ b/tests/suites/middleware/ProcessStackTest.php @@ -50,6 +50,7 @@ public function testIncoming(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware(new Callback(incoming: function ($stack, $connection) { @@ -100,6 +101,7 @@ public function testOutgoing(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware(new Callback(outgoing: function ($stack, $connection, $message) { diff --git a/tests/suites/middleware/SubprotocolNegotiationTest.php b/tests/suites/middleware/SubprotocolNegotiationTest.php index f0b1434..a6c3c4b 100644 --- a/tests/suites/middleware/SubprotocolNegotiationTest.php +++ b/tests/suites/middleware/SubprotocolNegotiationTest.php @@ -58,6 +58,7 @@ public function testClientProtocolMatch(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware($middleware); @@ -110,6 +111,7 @@ public function testClientProtocolNoMatch(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware($middleware); @@ -159,6 +161,7 @@ public function testClientProtocolRequire(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware($middleware); @@ -207,6 +210,7 @@ public function testServerProtocolMatch(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware($middleware); @@ -265,6 +269,7 @@ public function testServerProtocolNoMatch(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware($middleware); @@ -324,6 +329,7 @@ public function testServerProtocolRequire(): void $this->expectSocketStreamGetLocalName(); $this->expectSocketStreamGetRemoteName(); + $this->expectSocketStreamSetTimeout(); $connection = new Connection($stream, false, false); $connection->addMiddleware($middleware); diff --git a/tests/suites/server/ConfigErrorTest.php b/tests/suites/server/ConfigErrorTest.php index c81b3fd..af22a20 100644 --- a/tests/suites/server/ConfigErrorTest.php +++ b/tests/suites/server/ConfigErrorTest.php @@ -64,6 +64,7 @@ public function testInvalidMaxConnections(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionCode(0); $this->expectExceptionMessage("Invalid maxConnections '0' provided"); + // @phpstan-ignore argument.type $server->setMaxConnections(0); } } diff --git a/tests/suites/server/ConfigTest.php b/tests/suites/server/ConfigTest.php index e3017b4..6990a53 100644 --- a/tests/suites/server/ConfigTest.php +++ b/tests/suites/server/ConfigTest.php @@ -25,6 +25,7 @@ use Phrity\Util\ErrorHandler; use Psr\Log\NullLogger; use WebSocket\{ + Configuration, Connection, Server, }; @@ -56,7 +57,7 @@ public function tearDown(): void $this->tearDownStack(); } - public function xxxtestServerDefaults(): void + public function testServerDefaults(): void { $this->expectStreamFactory(); $server = new Server(8000); @@ -215,4 +216,22 @@ public function testHttpFactories(): void unset($server); } + + public function testConfiguration(): void + { + $logger = new NullLogger(); + $this->expectContext(); + $context = new Context(); + $configuration = new Configuration( + logger: $logger, + context: $context, + timeout: 120, + frameSize: 64, + maxConnections: 1, + ); + $server = new Server(8000, configuration: $configuration); + $this->assertInstanceOf(Configuration::class, $server->getConfiguration()); + $this->assertSame($server, $server->setConfiguration($configuration)); + unset($server); + } } diff --git a/tests/suites/server/ServerTest.php b/tests/suites/server/ServerTest.php index 9603aca..c3c1208 100644 --- a/tests/suites/server/ServerTest.php +++ b/tests/suites/server/ServerTest.php @@ -387,7 +387,8 @@ public function testShutdown(): void $this->expectSocketStreamIsConnected(); $this->expectSocketStreamIsConnected(); - $this->expectWsSelectConnections(['*/connection/8000/12345', '*/connection/8000/23456']); + // The @server handler should be blocked now + $this->expectWsSelectConnections(['server/8000', '*/connection/8000/12345', '*/connection/8000/23456']); // Receive close ack connection 1 $this->expectSocketStreamRead()->addAssert(function (string $method, array $params) {