diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0a0a1c1..fd322b8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ jobs: - "8.1" - "8.2" - "8.3" + - "8.4" dependencies: - "highest" include: diff --git a/README.md b/README.md index 317d158a..4f2e349a 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ composer require cboden/ratchet:^0.4.4 See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP -extensions and supports running on legacy PHP 5.4 through PHP 8.3+ with limited support for newer PHP. +extensions and supports running on legacy PHP 5.4 through current PHP 8+. It's *highly recommended to use the latest supported PHP version* for this project. See above note about [Reviving Ratchet](#reviving-ratchet) for newer PHP support. diff --git a/src/Ratchet/App.php b/src/Ratchet/App.php index d3de200a..f2fa55cc 100644 --- a/src/Ratchet/App.php +++ b/src/Ratchet/App.php @@ -57,16 +57,19 @@ class App { protected $_routeCounter = 0; /** - * @param string $httpHost HTTP hostname clients intend to connect to. MUST match JS `new WebSocket('ws://$httpHost');` - * @param int $port Port to listen on. If 80, assuming production, Flash on 843 otherwise expecting Flash to be proxied through 8843 - * @param string $address IP address to bind to. Default is localhost/proxy only. '0.0.0.0' for any machine. - * @param LoopInterface $loop Specific React\EventLoop to bind the application to. null will create one for you. - * @param array $context + * @param string $httpHost HTTP hostname clients intend to connect to. MUST match JS `new WebSocket('ws://$httpHost');` + * @param int $port Port to listen on. If 80, assuming production, Flash on 843 otherwise expecting Flash to be proxied through 8843 + * @param string $address IP address to bind to. Default is localhost/proxy only. '0.0.0.0' for any machine. + * @param ?LoopInterface $loop Specific React\EventLoop to bind the application to. null will create one for you. + * @param array $context */ - public function __construct($httpHost = 'localhost', $port = 8080, $address = '127.0.0.1', LoopInterface $loop = null, $context = array()) { + public function __construct($httpHost = 'localhost', $port = 8080, $address = '127.0.0.1', $loop = null, $context = array()) { if (extension_loaded('xdebug') && getenv('RATCHET_DISABLE_XDEBUG_WARN') === false) { trigger_error('XDebug extension detected. Remember to disable this if performance testing or going live!', E_USER_WARNING); } + if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1 + throw new \InvalidArgumentException('Argument #4 ($loop) expected null|React\EventLoop\LoopInterface'); + } if (null === $loop) { $loop = LoopFactory::create(); diff --git a/src/Ratchet/Http/HttpServerInterface.php b/src/Ratchet/Http/HttpServerInterface.php index 2c37c490..614fb52b 100644 --- a/src/Ratchet/Http/HttpServerInterface.php +++ b/src/Ratchet/Http/HttpServerInterface.php @@ -10,5 +10,6 @@ interface HttpServerInterface extends MessageComponentInterface { * @param \Psr\Http\Message\RequestInterface $request null is default because PHP won't let me overload; don't pass null!!! * @throws \UnexpectedValueException if a RequestInterface is not passed */ - public function onOpen(ConnectionInterface $conn, RequestInterface $request = null); + #[HackSupportForPHP8] public function onOpen(ConnectionInterface $conn, ?RequestInterface $request = null); /* + public function onOpen(ConnectionInterface $conn, RequestInterface $request = null); /**/ } diff --git a/src/Ratchet/Http/NoOpHttpServerController.php b/src/Ratchet/Http/NoOpHttpServerController.php index 4f72e668..5bb1f8e6 100644 --- a/src/Ratchet/Http/NoOpHttpServerController.php +++ b/src/Ratchet/Http/NoOpHttpServerController.php @@ -4,7 +4,8 @@ use Psr\Http\Message\RequestInterface; class NoOpHttpServerController implements HttpServerInterface { - public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { + #[HackSupportForPHP8] public function onOpen(ConnectionInterface $conn, ?RequestInterface $request = null) { /* + public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { /**/ } public function onMessage(ConnectionInterface $from, $msg) { diff --git a/src/Ratchet/Http/OriginCheck.php b/src/Ratchet/Http/OriginCheck.php index 2bdc0f7c..257c847b 100644 --- a/src/Ratchet/Http/OriginCheck.php +++ b/src/Ratchet/Http/OriginCheck.php @@ -31,7 +31,8 @@ public function __construct(MessageComponentInterface $component, array $allowed /** * {@inheritdoc} */ - public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { + #[HackSupportForPHP8] public function onOpen(ConnectionInterface $conn, ?RequestInterface $request = null) { /* + public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { /**/ $header = (string)$request->getHeader('Origin')[0]; $origin = parse_url($header, PHP_URL_HOST) ?: $header; diff --git a/src/Ratchet/Http/Router.php b/src/Ratchet/Http/Router.php index 2bd5942f..71e52f2c 100644 --- a/src/Ratchet/Http/Router.php +++ b/src/Ratchet/Http/Router.php @@ -26,7 +26,8 @@ public function __construct(UrlMatcherInterface $matcher) { * {@inheritdoc} * @throws \UnexpectedValueException If a controller is not \Ratchet\Http\HttpServerInterface */ - public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { + #[HackSupportForPHP8] public function onOpen(ConnectionInterface $conn, ?RequestInterface $request = null) { /* + public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { /**/ if (null === $request) { throw new \UnexpectedValueException('$request can not be null'); } diff --git a/src/Ratchet/Server/IoServer.php b/src/Ratchet/Server/IoServer.php index 0e84ae41..194a3317 100644 --- a/src/Ratchet/Server/IoServer.php +++ b/src/Ratchet/Server/IoServer.php @@ -34,7 +34,11 @@ class IoServer { * @param \React\Socket\ServerInterface $socket The React socket server to run the Ratchet application off of * @param \React\EventLoop\LoopInterface|null $loop The React looper to run the Ratchet application off of */ - public function __construct(MessageComponentInterface $app, ServerInterface $socket, LoopInterface $loop = null) { + public function __construct(MessageComponentInterface $app, ServerInterface $socket, $loop = null) { + if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1 + throw new \InvalidArgumentException('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface'); + } + if (false === strpos(PHP_VERSION, "hiphop")) { gc_enable(); } diff --git a/src/Ratchet/Session/SessionProvider.php b/src/Ratchet/Session/SessionProvider.php index 44276c54..76364e2b 100644 --- a/src/Ratchet/Session/SessionProvider.php +++ b/src/Ratchet/Session/SessionProvider.php @@ -38,13 +38,16 @@ class SessionProvider implements HttpServerInterface { protected $_serializer; /** - * @param \Ratchet\Http\HttpServerInterface $app - * @param \SessionHandlerInterface $handler - * @param array $options - * @param \Ratchet\Session\Serialize\HandlerInterface $serializer + * @param \Ratchet\Http\HttpServerInterface $app + * @param \SessionHandlerInterface $handler + * @param array $options + * @param ?\Ratchet\Session\Serialize\HandlerInterface $serializer * @throws \RuntimeException */ - public function __construct(HttpServerInterface $app, \SessionHandlerInterface $handler, array $options = array(), HandlerInterface $serializer = null) { + public function __construct(HttpServerInterface $app, \SessionHandlerInterface $handler, array $options = array(), $serializer = null) { + if ($serializer !== null && !$serializer instanceof HandlerInterface) { // manual type check to support legacy PHP < 7.1 + throw new \InvalidArgumentException('Argument #4 ($serializer) expected null|Ratchet\Session\Serialize\HandlerInterface'); + } $this->_app = $app; $this->_handler = $handler; $this->_null = new NullSessionHandler; @@ -70,7 +73,8 @@ public function __construct(HttpServerInterface $app, \SessionHandlerInterface $ /** * {@inheritdoc} */ - public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { + #[HackSupportForPHP8] public function onOpen(ConnectionInterface $conn, ?RequestInterface $request = null) { /* + public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { /**/ $sessionName = ini_get('session.name'); $id = array_reduce($request->getHeader('Cookie'), function($accumulator, $cookie) use ($sessionName) { diff --git a/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index 27795ca7..5a015d83 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/WsServer.php @@ -104,7 +104,8 @@ public function __construct(ComponentInterface $component) { /** * {@inheritdoc} */ - public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { + #[HackSupportForPHP8] public function onOpen(ConnectionInterface $conn, ?RequestInterface $request = null) { /* + public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { /**/ if (null === $request) { throw new \UnexpectedValueException('$request can not be null'); } diff --git a/tests/helpers/Ratchet/Mock/Component.php b/tests/helpers/Ratchet/Mock/Component.php deleted file mode 100644 index e152988b..00000000 --- a/tests/helpers/Ratchet/Mock/Component.php +++ /dev/null @@ -1,35 +0,0 @@ -last[__FUNCTION__] = func_get_args(); - } - - public function onOpen(ConnectionInterface $conn) { - $this->last[__FUNCTION__] = func_get_args(); - } - - public function onMessage(ConnectionInterface $from, $msg) { - $this->last[__FUNCTION__] = func_get_args(); - } - - public function onClose(ConnectionInterface $conn) { - $this->last[__FUNCTION__] = func_get_args(); - } - - public function onError(ConnectionInterface $conn, \Exception $e) { - $this->last[__FUNCTION__] = func_get_args(); - } - - public function getSubProtocols() { - return $this->protocols; - } -} diff --git a/tests/unit/AppTest.php b/tests/unit/AppTest.php new file mode 100644 index 00000000..7e058e38 --- /dev/null +++ b/tests/unit/AppTest.php @@ -0,0 +1,17 @@ +expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Argument #4 ($loop) expected null|React\EventLoop\LoopInterface'); + } else { + $this->setExpectedException('InvalidArgumentException', 'Argument #4 ($loop) expected null|React\EventLoop\LoopInterface'); + } + new App('localhost', 8080, '127.0.0.1', 'loop'); + } +} diff --git a/tests/unit/Server/IoServerTest.php b/tests/unit/Server/IoServerTest.php index f337f1ea..0264f3fc 100644 --- a/tests/unit/Server/IoServerTest.php +++ b/tests/unit/Server/IoServerTest.php @@ -39,6 +39,16 @@ public function setUpServer() { $this->server = new IoServer($this->app, $this->reactor, $loop); } + public function testCtorThrowsForInvalidLoop() { + if (method_exists($this, 'expectException')) { + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface'); + } else { + $this->setExpectedException('InvalidArgumentException', 'Argument #3 ($loop) expected null|React\EventLoop\LoopInterface'); + } + new IoServer($this->app, $this->reactor, 'loop'); + } + public function testOnOpen() { $this->app->expects($this->once())->method('onOpen')->with($this->isInstanceOf('Ratchet\\ConnectionInterface')); diff --git a/tests/unit/Session/SessionComponentTest.php b/tests/unit/Session/SessionProviderTest.php similarity index 89% rename from tests/unit/Session/SessionComponentTest.php rename to tests/unit/Session/SessionProviderTest.php index 98077dad..2bec51b0 100644 --- a/tests/unit/Session/SessionComponentTest.php +++ b/tests/unit/Session/SessionProviderTest.php @@ -24,6 +24,16 @@ public function setUpProvider() { $this->_serv = new SessionProvider($this->_app, new NullSessionHandler); } + public function testCtorThrowsForInvalidSerializer() { + if (method_exists($this, 'expectException')) { + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Argument #4 ($serializer) expected null|Ratchet\Session\Serialize\HandlerInterface'); + } else { + $this->setExpectedException('InvalidArgumentException', 'Argument #4 ($serializer) expected null|Ratchet\Session\Serialize\HandlerInterface'); + } + new SessionProvider($this->_app, new NullSessionHandler(), [], 'serializer'); + } + /** * @after */