From 912fa997d1b0af7a0ec45250729bdc17dde2957e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 27 Mar 2023 12:42:49 +0400 Subject: [PATCH 1/7] Make HTTP stream stoppable; add phpunit; up PHP version to 8.1 --- .gitignore | 1 + composer.json | 25 ++++----- phpunit.xml | 28 ++++++++++ src/HttpWorker.php | 5 ++ tests/Unit/StreamResponseTest.php | 91 +++++++++++++++++++++++++++++++ tests/Unit/Stub/TestRelay.php | 75 +++++++++++++++++++++++++ 6 files changed, 212 insertions(+), 13 deletions(-) create mode 100644 phpunit.xml create mode 100644 tests/Unit/StreamResponseTest.php create mode 100644 tests/Unit/Stub/TestRelay.php diff --git a/.gitignore b/.gitignore index ce65a4b..92721e3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ # vendor/ .idea vendor +.phpunit.result.cache composer.lock diff --git a/composer.json b/composer.json index ab294bd..8ced807 100644 --- a/composer.json +++ b/composer.json @@ -14,33 +14,32 @@ } ], "require": { - "php": ">=7.4", + "php": ">=8.1", "ext-json": "*", "spiral/roadrunner-worker": "^2.2.0", "psr/http-factory": "^1.0.1", "psr/http-message": "^1.0.1" }, - "autoload": { - "psr-4": { - "Spiral\\RoadRunner\\Http\\": "src" - } - }, "require-dev": { "nyholm/psr7": "^1.3", - "phpstan/phpstan": "~0.12", - "phpunit/phpunit": "~8.0", + "phpunit/phpunit": "^9.5", "jetbrains/phpstorm-attributes": "^1.0", "vimeo/psalm": "^4.22", "symfony/var-dumper": "^5.1" }, - "scripts": { - "analyze": "psalm" + "autoload": { + "psr-4": { + "Spiral\\RoadRunner\\Http\\": "src" + } }, - "extra": { - "branch-alias": { - "dev-master": "2.2.x-dev" + "autoload-dev": { + "psr-4": { + "Spiral\\RoadRunner\\Tests\\Http\\": "tests" } }, + "scripts": { + "analyze": "psalm" + }, "suggest": { "spiral/roadrunner-cli": "Provides RoadRunner installation and management CLI tools" }, diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..bc5538c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,28 @@ + + + + + tests + + + + + + src + + + diff --git a/src/HttpWorker.php b/src/HttpWorker.php index 097f602..fcf9803 100644 --- a/src/HttpWorker.php +++ b/src/HttpWorker.php @@ -12,6 +12,7 @@ namespace Spiral\RoadRunner\Http; use Generator; +use Spiral\RoadRunner\Message\Command\StreamStop; use Spiral\RoadRunner\Payload; use Spiral\RoadRunner\WorkerInterface; use Stringable; @@ -114,6 +115,10 @@ public function respondStream(int $status, Generator $body, array $headers = []) break; } $content = (string)$body->current(); + if ($this->worker->getPayload(StreamStop::class) !== null) { + $body->throw(new \RuntimeException('Stream has been stopped by the client.')); + return; + } $this->worker->respond(new Payload($content, $head, false)); $body->next(); $head = null; diff --git a/tests/Unit/StreamResponseTest.php b/tests/Unit/StreamResponseTest.php new file mode 100644 index 0000000..9113709 --- /dev/null +++ b/tests/Unit/StreamResponseTest.php @@ -0,0 +1,91 @@ +relay, $this->worker); + parent::tearDown(); + } + + /** + * Regular case + */ + public function testRegularCase(): void + { + $worker = $this->getWorker(); + $this->getRelay() + ->addFrame(status: 200, body: 'Hello, World!', headers: ['Content-Type' => 'text/plain'], stream: true); + + self::assertTrue($worker->hasPayload()); + self::assertInstanceOf(Payload::class, $payload = $worker->waitPayload()); + self::assertSame('Hello, World!', $payload->body); + } + + /** + * Test stream response with multiple frames + */ + public function testStreamResponseWithMultipleFrames(): void + { + $httpWorker = $this->makeHttpWorker(); + + $httpWorker->respondStream(200, (function () { + yield 'Hel'; + yield 'lo,'; + yield ' Wo'; + yield 'rld'; + yield '!'; + })()); + + self::assertFalse($this->worker->hasPayload()); + self::assertSame('Hello, World!', $this->getRelay()->getReceivedBody()); + } + + public function testStopStreamResponse(): void + { + $httpWorker = $this->makeHttpWorker(); + + $httpWorker->respondStream(200, (function () { + yield 'Hel'; + yield 'lo,'; + $this->getRelay()->addStopStreamFrame(); + try { + yield ' Wo'; + } catch (\Throwable $e) { + return; + } + yield 'rld'; + yield '!'; + })()); + + self::assertSame('Hello,', $this->getRelay()->getReceivedBody()); + } + + private function getRelay(): TestRelay + { + return $this->relay ??= new TestRelay(); + } + + private function getWorker(): Worker + { + return $this->worker ??= new Worker($this->getRelay(), false); + } + + private function makeHttpWorker(): HttpWorker + { + return new HttpWorker($this->getWorker()); + } +} \ No newline at end of file diff --git a/tests/Unit/Stub/TestRelay.php b/tests/Unit/Stub/TestRelay.php new file mode 100644 index 0000000..4b2cd34 --- /dev/null +++ b/tests/Unit/Stub/TestRelay.php @@ -0,0 +1,75 @@ +frames = [...$this->frames, ...\array_values($frames)]; + return $this; + } + + public function addFrame( + int $status = 200, + string $body = '', + array $headers = [], + bool $stream = false, + bool $stopStream = false, + ): self { + $head = (string)\json_encode([ + 'status' => $status, + 'headers' => $headers, + ], \JSON_THROW_ON_ERROR); + $frame = new Frame($head .$body, [\strlen($head)]); + $frame->byte10 |= $stream ? Frame::BYTE10_STREAM : 0; + $frame->byte10 |= $stopStream ? Frame::BYTE10_STOP : 0; + return $this->addFrames($frame); + } + + public function addStopStreamFrame(): self + { + return $this->addFrame(stopStream: true); + } + + public function getReceived(): array + { + return $this->received; + } + + public function getReceivedBody(): string + { + return \implode('', \array_map(static fn (Frame $frame) + => \substr($frame->payload, $frame->options[0]), $this->received)); + } + + public function waitFrame(): Frame + { + if ($this->frames === []) { + throw new \RuntimeException('There are no frames to return.'); + } + + return \array_shift($this->frames); + } + + public function send(Frame $frame): void + { + $this->received[] = $frame; + } + + public function hasFrame(): bool + { + return $this->frames !== []; + } +} From 4d1423fb2c1adff53edfa99c08ba8609370e5ab8 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 28 Mar 2023 00:40:51 +0400 Subject: [PATCH 2/7] Add test server; add feature tests --- composer.json | 5 +- tests/Feature/StreamResponseTest.php | 117 ++++++++++++++++++ tests/Server/Client.php | 173 +++++++++++++++++++++++++++ tests/Server/Command/BaseCommand.php | 29 +++++ tests/Server/Command/StreamStop.php | 18 +++ tests/Server/Server.php | 69 +++++++++++ tests/Server/ServerRunner.php | 61 ++++++++++ tests/Server/run_server.php | 14 +++ 8 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/StreamResponseTest.php create mode 100644 tests/Server/Client.php create mode 100644 tests/Server/Command/BaseCommand.php create mode 100644 tests/Server/Command/StreamStop.php create mode 100644 tests/Server/Server.php create mode 100644 tests/Server/ServerRunner.php create mode 100644 tests/Server/run_server.php diff --git a/composer.json b/composer.json index 8ced807..e5409f5 100644 --- a/composer.json +++ b/composer.json @@ -16,9 +16,10 @@ "require": { "php": ">=8.1", "ext-json": "*", - "spiral/roadrunner-worker": "^2.2.0", "psr/http-factory": "^1.0.1", - "psr/http-message": "^1.0.1" + "psr/http-message": "^1.0.1", + "spiral/roadrunner-worker": "^2.2.0", + "symfony/process": "^6.2" }, "require-dev": { "nyholm/psr7": "^1.3", diff --git a/tests/Feature/StreamResponseTest.php b/tests/Feature/StreamResponseTest.php new file mode 100644 index 0000000..afb5928 --- /dev/null +++ b/tests/Feature/StreamResponseTest.php @@ -0,0 +1,117 @@ +relay, $this->worker); + ServerRunner::stop(); + parent::tearDown(); + } + + /** + * Regular case + */ + public function testRegularCase(): void + { + $worker = $this->getWorker(); + $worker->respond(new Payload('Hello, World!')); + + \usleep(100_000); + self::assertSame('Hello, World!', \trim(ServerRunner::getBuffer())); + } + + /** + * Test stream response with multiple frames + */ + public function testStreamResponseWithMultipleFrames(): void + { + $httpWorker = $this->makeHttpWorker(); + + $chunks = ['Hel', 'lo,', ' Wo', 'rld', '!']; + ServerRunner::getBuffer(); + $httpWorker->respondStream( + 200, + (function () use ($chunks) { + yield from $chunks; + })(), + ); + + \usleep(100_000); + self::assertSame(\implode("\n", $chunks), \trim(ServerRunner::getBuffer())); + } + + public function testStopStreamResponse(): void + { + $httpWorker = $this->makeHttpWorker(); + + // Flush buffer + ServerRunner::getBuffer(); + + $httpWorker->respondStream( + 200, + (function () { + yield 'Hel'; + yield 'lo,'; + $this->sendCommand(new StreamStop()); + try { + yield ' Wo'; + } catch (\Throwable $e) { + return; + } + yield 'rld'; + yield '!'; + })(), + ); + + + \usleep(100_000); + self::assertSame(\implode("\n", ['Hel', 'lo,']), \trim(ServerRunner::getBuffer())); + } + + private function getRelay(): SocketRelay + { + return $this->relay ??= SocketRelay::create($this->serverAddress); + } + + private function getWorker(): Worker + { + return $this->worker ??= new Worker($this->getRelay(), false); + } + + private function makeHttpWorker(): HttpWorker + { + return new HttpWorker($this->getWorker()); + } + + private function sendCommand(BaseCommand $command) + { + $this->getRelay()->send($command->getRequestFrame()); + \usleep(500_000); + } +} \ No newline at end of file diff --git a/tests/Server/Client.php b/tests/Server/Client.php new file mode 100644 index 0000000..e5dc0c7 --- /dev/null +++ b/tests/Server/Client.php @@ -0,0 +1,173 @@ +socket = $socket; + \socket_set_nonblock($this->socket); + } + + public function __destruct() + { + \socket_close($this->socket); + } + + public static function init(\Socket $socket): self + { + return new self($socket); + } + + public function process(): void + { + $this->onInit(); + + do { + $read = [$this->socket]; + $write = [$this->socket]; + $except = [$this->socket]; + if (\socket_select($read, $write, $except, 0, 0) === false) { + throw new \RuntimeException('Socket select failed.'); + } + + if ($read !== []) { + $this->readMessage(); + } + + if ($write !== [] && $this->writeQueue !== []) { + $this->writeQueue(); + } + + Fiber::suspend(); + } while (true); + } + + private function onInit() + { + $this->writeQueue[] = Frame::packFrame(new Frame('{"pid":true}', [], Frame::CONTROL)); + } + + private function onFrame(Frame $frame): void + { + $command = $this->getCommand($frame); + + if ($command === null) { + echo \substr($frame->payload, $frame->options[0]) . "\n"; + return; + } + + $this->onCommand($command); + } + + private function writeQueue(): void + { + foreach ($this->writeQueue as $data) { + \socket_write($this->socket, $data); + } + socket_set_nonblock($this->socket); + + $this->writeQueue = []; + } + + /** + * @see \Spiral\Goridge\SocketRelay::waitFrame() + */ + private function readMessage(): void + { + $header = $this->readNBytes(12); + + $parts = Frame::readHeader($header); + // total payload length + $length = $parts[1] * 4 + $parts[2]; + + if ($length >= 8 * 1024 * 1024) { + throw new \RuntimeException('Frame payload is too large.'); + } + $payload = $this->readNBytes($length); + + $frame = Frame::initFrame($parts, $payload); + + $this->onFrame($frame); + } + + /** + * @param positive-int $bytes + * + * @return non-empty-string + */ + private function readNBytes(int $bytes, bool $canBeLess = false): string + { + while (($left = $bytes - \strlen($this->readBuffer)) > 0) { + $data = @\socket_read($this->socket, $left, \PHP_BINARY_READ); + if ($data === false) { + $errNo = \socket_last_error($this->socket); + throw new \RuntimeException('Socket read failed [' . $errNo . ']: ' . \socket_strerror($errNo)); + } + + if ($canBeLess) { + return $data; + } + + if ($data === '') { + Fiber::suspend(); + continue; + } + + $this->readBuffer .= $data; + } + + $result = \substr($this->readBuffer, 0, $bytes); + $this->readBuffer = \substr($this->readBuffer, $bytes); + + return $result; + } + + private function getCommand(Frame $frame): ?BaseCommand + { + $payload = $frame->payload; + try { + $data = \json_decode($payload, true, 3, \JSON_THROW_ON_ERROR); + } catch (\JsonException) { + return null; + } + + return match (false) { + \is_array($data), + \array_key_exists(BaseCommand::COMMAND_KEY, $data), + \is_string($data[BaseCommand::COMMAND_KEY]), + \class_exists($data[BaseCommand::COMMAND_KEY]), + \is_a($data[BaseCommand::COMMAND_KEY], BaseCommand::class, true) => null, + default => new ($data[BaseCommand::COMMAND_KEY])(), + }; + } + + private function onCommand(BaseCommand $command): void + { + switch ($command::class) { + case StreamStop::class: + $this->writeQueue[] = $command->getResponse(); + break; + } + } +} \ No newline at end of file diff --git a/tests/Server/Command/BaseCommand.php b/tests/Server/Command/BaseCommand.php new file mode 100644 index 0000000..f36e7fd --- /dev/null +++ b/tests/Server/Command/BaseCommand.php @@ -0,0 +1,29 @@ +frame = new Frame(\json_encode([self::COMMAND_KEY => static::class])); + } + + public function getRequestFrame(): Frame + { + return $this->frame; + } + + public function getResponse(): string + { + return Frame::packFrame($this->getResponseFrame()); + } + + public abstract function getResponseFrame(): Frame; +} \ No newline at end of file diff --git a/tests/Server/Command/StreamStop.php b/tests/Server/Command/StreamStop.php new file mode 100644 index 0000000..34a3e56 --- /dev/null +++ b/tests/Server/Command/StreamStop.php @@ -0,0 +1,18 @@ +byte10 |= Frame::BYTE10_STOP; + + return $frame; + } +} diff --git a/tests/Server/Server.php b/tests/Server/Server.php new file mode 100644 index 0000000..2e1f865 --- /dev/null +++ b/tests/Server/Server.php @@ -0,0 +1,69 @@ +socket = \socket_create_listen($port); + if ($this->socket === false) { + throw new \RuntimeException('Socket create failed.'); + } + \socket_set_nonblock($this->socket); + + echo "Server started\n"; + } + + public function __destruct() + { + \socket_close($this->socket); + } + + public static function init(int $port = 6002): self + { + return new self($port); + } + + public function process(): void + { + $client = \socket_accept($this->socket); + if ($client !== false) { + $key = \array_key_last($this->clients) + 1; + try { + $this->clients[$key] = Client::init($client); + $this->fibers[$key] = new Fiber($this->clients[$key]->process(...)); + } catch (\Throwable) { + unset($this->clients[$key], $this->fibers[$key]); + } + } + + foreach ($this->fibers as $key => $fiber) { + try { + $fiber->isStarted() ? $fiber->resume() : $fiber->start(); + + if ($fiber->isTerminated()) { + throw new RuntimeException('Client terminated.'); + } + } catch (\Throwable) { + unset($this->clients[$key], $this->fibers[$key]); + } + } + } +} diff --git a/tests/Server/ServerRunner.php b/tests/Server/ServerRunner.php new file mode 100644 index 0000000..0ba49f8 --- /dev/null +++ b/tests/Server/ServerRunner.php @@ -0,0 +1,61 @@ +setTimeout($timeout); + self::$process->start(static function (string $type, string $output) use (&$run) { + if (!$run && $type === Process::OUT && \str_contains($output, 'Server started')) { + $run = true; + } + if ($type === Process::OUT) { + self::$output .= $output; + } + // echo $output; + }); + + if (!self::$process->isRunning()) { + throw new RuntimeException('Error starting Server: ' . self::$process->getErrorOutput()); + } + + // wait for roadrunner to start + $ticks = $timeout * 10; + while (!$run && $ticks > 0) { + self::$process->getStatus(); + \usleep(100000); + --$ticks; + } + + if (!$run) { + throw new RuntimeException('Error starting Server: timeout'); + } + } + + public static function stop(): void + { + self::$process?->stop(0, 0); + } + + public static function getBuffer(): string + { + self::$process->getStatus(); + $result = self::$output; + self::$output = ''; + return $result; + } +} diff --git a/tests/Server/run_server.php b/tests/Server/run_server.php new file mode 100644 index 0000000..81d2652 --- /dev/null +++ b/tests/Server/run_server.php @@ -0,0 +1,14 @@ +process(); + \usleep(5_000); +} From d46dcc48e0267657a6a616febdf23f199b275033 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 29 Mar 2023 21:36:13 +0400 Subject: [PATCH 3/7] Add metafiles; update dependencies; add CI --- .editorconfig | 15 +++++++++++++ .gitattributes | 10 +++++++++ .github/CODE_OF_CONDUCT.md | 12 ++++++++++ .github/CONTRIBUTING.md | 6 +++++ .github/ISSUE_TEMPLATE/1_Bug_report.md | 23 ++++++++++++++++++++ .github/ISSUE_TEMPLATE/2_Feature_request.md | 14 ++++++++++++ .github/ISSUE_TEMPLATE/3_Support_question.md | 15 +++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 10 +++++++++ .github/SECURITY.md | 11 ++++++++++ .github/workflows/phpunit.yml | 19 ++++++++++++++++ .github/workflows/psalm.yml | 17 +++++++++++++++ composer.json | 10 ++++----- psalm.xml | 6 +++-- 13 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/1_Bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/2_Feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/3_Support_question.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/SECURITY.md create mode 100644 .github/workflows/phpunit.yml create mode 100644 .github/workflows/psalm.yml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a998937 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.{yml, yaml, sh, conf, neon*}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bc4b7e4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +* text=auto + +/.github export-ignore +/tests export-ignore +/.* export-ignore +/phpunit.xml* export-ignore +/phpstan.* export-ignore +/psalm.* export-ignore +/infection.* export-ignore +/codecov.* export-ignore diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5d589cb --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,12 @@ +# Code of Conduct + +The Spiral code of conduct is derived from the Ruby code of conduct. +Any violations of the code of conduct may be reported by email `wolfy-j[at]spiralscout.com`. + +- Participants will be tolerant of opposing views. + +- Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks. + +- When interpreting the words and actions of others, participants should always assume good intentions. + +- Behavior which can be reasonably considered harassment will not be tolerated. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..90e5310 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# Contributing + +Feel free to contribute to the development of the Framework or its components. + +For more information on contributing rules you can find on the documentation +page at https://spiral.dev/docs/about-contributing diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md new file mode 100644 index 0000000..5ea3ac3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.md @@ -0,0 +1,23 @@ +--- +name: 🐛 Bug Report +about: Report errors and problems +labels: Bug + +--- +### Description + + + +### How To Reproduce + + + +### Additional Info + +| Q | A +|------------------| --- +| Package Version | x.y.z +| PHP version | x.y.z +| Operating system | Linux + + diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md new file mode 100644 index 0000000..4a14c3d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_Feature_request.md @@ -0,0 +1,14 @@ +--- +name: 🚀 Feature Request +about: RFC and ideas for new features and improvements +labels: Feature + +--- +## Description + + + +## Example + + diff --git a/.github/ISSUE_TEMPLATE/3_Support_question.md b/.github/ISSUE_TEMPLATE/3_Support_question.md new file mode 100644 index 0000000..7d0ecb4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3_Support_question.md @@ -0,0 +1,15 @@ +--- +name: ❓ Question +about: Use if you have problems and don't know how to formulate them +labels: Question + +--- + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8c0643a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +| Q | A +| ------------- | --- +| Bugfix? | ✔️/❌ +| Breaks BC? | ✔️/❌ +| New feature? | ✔️/❌ +| Issues | #... +| Docs PR | spiral/docs#... + + diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..259a7a0 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,11 @@ +Security Policy +=============== + +If you found something which shouldn't be there or a bug which opens a security +hole, please let me know immediately by email `wolfy-j[at]spiralscout.com`. + +DO NOT PUBLISH SECURITY REPORTS PUBLICLY. + +The full [Security Policy][1] is described in the official documentation. + + [1]: https://spiral.dev/docs/about-contributing#critical-security-issues diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..854efaa --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,19 @@ +on: + push: + branches: + - master + - '*.*' + pull_request: null + +name: phpunit + +jobs: + phpunit: + uses: spiral/gh-actions/.github/workflows/phpunit.yml@master + with: + os: >- + ['ubuntu-latest'] + php: >- + ['8.1', '8.2'] + stability: >- + ['prefer-stable', 'prefer-lowest'] diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml new file mode 100644 index 0000000..bdb570c --- /dev/null +++ b/.github/workflows/psalm.yml @@ -0,0 +1,17 @@ +on: + push: + branches: + - master + - '*.*' + pull_request: null + +name: static analysis + +jobs: + psalm: + uses: spiral/gh-actions/.github/workflows/psalm.yml@master + with: + os: >- + ['ubuntu-latest'] + php: >- + ['8.1'] diff --git a/composer.json b/composer.json index e5409f5..5c5e075 100644 --- a/composer.json +++ b/composer.json @@ -18,15 +18,15 @@ "ext-json": "*", "psr/http-factory": "^1.0.1", "psr/http-message": "^1.0.1", - "spiral/roadrunner-worker": "^2.2.0", - "symfony/process": "^6.2" + "spiral/roadrunner": "^2023.1", + "spiral/roadrunner-worker": "^2.2.0" }, "require-dev": { "nyholm/psr7": "^1.3", - "phpunit/phpunit": "^9.5", + "phpunit/phpunit": "^10.0", "jetbrains/phpstorm-attributes": "^1.0", - "vimeo/psalm": "^4.22", - "symfony/var-dumper": "^5.1" + "symfony/process": "^6.2", + "vimeo/psalm": "^5.8" }, "autoload": { "psr-4": { diff --git a/psalm.xml b/psalm.xml index 6716a38..817a397 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,10 +1,12 @@ From dd8cd3471942cbff0b180ef3832a92f78192ccec Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 30 Mar 2023 00:40:09 +0400 Subject: [PATCH 4/7] Update classes and interfaces --- .gitignore | 21 +++++-- src/HttpWorker.php | 92 +++++++++------------------- src/HttpWorkerInterface.php | 12 +++- src/PSR7Worker.php | 20 +++--- src/PSR7WorkerInterface.php | 5 -- src/Request.php | 81 ++++++++---------------- tests/Feature/StreamResponseTest.php | 6 +- tests/Unit/StreamResponseTest.php | 6 +- 8 files changed, 94 insertions(+), 149 deletions(-) diff --git a/.gitignore b/.gitignore index 92721e3..4c64f4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,17 @@ -# Dependency directories (remove the comment below to include it) -# vendor/ -.idea -vendor -.phpunit.result.cache +# Composer lock file composer.lock + +# IDEs +/.idea +/.vscode + +# Vendors +/vendor +**/vendor + +# Temp dirs & trash +/tests/server* +clover* +cover* +.DS_Store +*.cache diff --git a/src/HttpWorker.php b/src/HttpWorker.php index fcf9803..24b746f 100644 --- a/src/HttpWorker.php +++ b/src/HttpWorker.php @@ -15,7 +15,6 @@ use Spiral\RoadRunner\Message\Command\StreamStop; use Spiral\RoadRunner\Payload; use Spiral\RoadRunner\WorkerInterface; -use Stringable; /** * @psalm-import-type HeadersList from Request @@ -40,29 +39,17 @@ */ class HttpWorker implements HttpWorkerInterface { - /** - * @var WorkerInterface - */ - private WorkerInterface $worker; - - /** - * @param WorkerInterface $worker - */ - public function __construct(WorkerInterface $worker) - { - $this->worker = $worker; + public function __construct( + private readonly WorkerInterface $worker, + ) { } - /** - * @return WorkerInterface - */ public function getWorker(): WorkerInterface { return $this->worker; } /** - * {@inheritDoc} * @throws \JsonException */ public function waitRequest(): ?Request @@ -81,11 +68,15 @@ public function waitRequest(): ?Request } /** - * {@inheritDoc} * @throws \JsonException */ - public function respond(int $status, string $body, array $headers = []): void + public function respond(int $status, string|Generator $body, array $headers = []): void { + if ($body instanceof Generator) { + $this->respondStream($status, $body, $headers); + return; + } + $head = (string)\json_encode([ 'status' => $status, 'headers' => $headers ?: (object)[], @@ -94,14 +85,7 @@ public function respond(int $status, string $body, array $headers = []): void $this->worker->respond(new Payload($body, $head)); } - /** - * Respond data using Streamed Output - * - * @param Generator $body Body generator. - * Each yielded value will be sent as a separated stream chunk. - * Returned value will be sent as a last stream package. - */ - public function respondStream(int $status, Generator $body, array $headers = []): void + private function respondStream(int $status, Generator $body, array $headers = []): void { $head = (string)\json_encode([ 'status' => $status, @@ -126,50 +110,33 @@ public function respondStream(int $status, Generator $body, array $headers = []) } /** - * @param string $body * @param RequestContext $context - * @return Request - * - * @psalm-suppress InaccessibleProperty */ private function createRequest(string $body, array $context): Request { - $request = new Request(); - $request->body = $body; - - $this->hydrateRequest($request, $context); - - return $request; - } - - /** - * @param Request $request - * @param RequestContext $context - * - * @psalm-suppress InaccessibleProperty - * @psalm-suppress MixedPropertyTypeCoercion - */ - private function hydrateRequest(Request $request, array $context): void - { - $request->remoteAddr = $context['remoteAddr']; - $request->protocol = $context['protocol']; - $request->method = $context['method']; - $request->uri = $context['uri']; - \parse_str($context['rawQuery'], $request->query); - - $request->attributes = (array)($context['attributes'] ?? []); - $request->headers = $this->filterHeaders((array)($context['headers'] ?? [])); - $request->cookies = (array)($context['cookies'] ?? []); - $request->uploads = (array)($context['uploads'] ?? []); - $request->parsed = (bool)$context['parsed']; - - $request->attributes[Request::PARSED_BODY_ATTRIBUTE_NAME] = $request->parsed; + \parse_str($context['rawQuery'], $query); + return new Request( + remoteAddr: $context['remoteAddr'], + protocol: $context['protocol'], + method: $context['method'], + uri: $context['uri'], + headers: $this->filterHeaders((array)($context['headers'] ?? [])), + cookies: (array)($context['cookies'] ?? []), + uploads: (array)($context['uploads'] ?? []), + attributes: [ + Request::PARSED_BODY_ATTRIBUTE_NAME => (bool)$context['parsed'], + ] + (array)($context['attributes'] ?? []), + query: $query, + body: $body, + parsed: (bool)$context['parsed'], + ); } /** * Remove all non-string and empty-string keys * - * @return array + * @param array> $headers + * @return HeadersList */ private function filterHeaders(array $headers): array { @@ -180,7 +147,8 @@ private function filterHeaders(array $headers): array unset($headers[$key]); } } - /** @var array $headers */ + + /** @var HeadersList $headers */ return $headers; } } diff --git a/src/HttpWorkerInterface.php b/src/HttpWorkerInterface.php index 20f58b4..8eef4d1 100644 --- a/src/HttpWorkerInterface.php +++ b/src/HttpWorkerInterface.php @@ -11,7 +11,9 @@ namespace Spiral\RoadRunner\Http; +use Generator; use Spiral\RoadRunner\WorkerAwareInterface; +use Stringable; /** * @psalm-import-type HeadersList from Request @@ -28,10 +30,14 @@ public function waitRequest(): ?Request; /** * Send response to the application server. * - * @param int $status Http status code - * @param string $body Body of response + * @param int $status Http status code + * @param Generator|string $body Body of response. + * If the body is a generator, then each yielded value will be sent as a separated stream chunk. + * Returned value will be sent as a last stream package. + * Note: Stream response is experimental feature and isn't supported by RoadRunner yet. + * But you can try to use RoadRunner 2.9-alpha to test it. * @param HeadersList|array $headers An associative array of the message's headers. Each key MUST be a header name, * and each value MUST be an array of strings for that header. */ - public function respond(int $status, string $body, array $headers = []): void; + public function respond(int $status, string|Generator $body, array $headers = []): void; } diff --git a/src/PSR7Worker.php b/src/PSR7Worker.php index cbeabaa..7350a74 100644 --- a/src/PSR7Worker.php +++ b/src/PSR7Worker.php @@ -33,6 +33,7 @@ class PSR7Worker implements PSR7WorkerInterface /** * @var int Preferred chunk size for streaming output. * if not greater than 0, then streaming response is turned off + * @internal */ public int $chunkSize = 0; @@ -117,18 +118,13 @@ public function waitRequest(): ?ServerRequestInterface */ public function respond(ResponseInterface $response): void { - if ($this->chunkSize > 0) { - $this->httpWorker->respondStream( - $response->getStatusCode(), - $this->streamToGenerator($response->getBody()), - $response->getHeaders() - ); - } else { - $this->httpWorker->respond( - $response->getStatusCode(), - (string)$response->getBody(), - $response->getHeaders()); - } + $this->httpWorker->respond( + $response->getStatusCode(), + $this->chunkSize > 0 + ? $this->streamToGenerator($response->getBody()) + : (string)$response->getBody(), + $response->getHeaders() + ); } /** diff --git a/src/PSR7WorkerInterface.php b/src/PSR7WorkerInterface.php index 137c4cd..dbb710e 100644 --- a/src/PSR7WorkerInterface.php +++ b/src/PSR7WorkerInterface.php @@ -17,15 +17,10 @@ interface PSR7WorkerInterface extends WorkerAwareInterface { - /** - * @return ServerRequestInterface|null - */ public function waitRequest(): ?ServerRequestInterface; /** * Send response to the application server. - * - * @param ResponseInterface $response */ public function respond(ResponseInterface $response): void; } diff --git a/src/Request.php b/src/Request.php index fdf182d..8b38af8 100644 --- a/src/Request.php +++ b/src/Request.php @@ -18,17 +18,19 @@ * * @psalm-type UploadedFile = array { * name: string, - * error: positive-int|0, + * error: int<0, max>, * tmpName: string, - * size: positive-int|0, + * size: int<0, max>, * mime: string * } * - * @psalm-type HeadersList = array> + * @psalm-type HeadersList = array> * @psalm-type AttributesList = array - * @psalm-type QueryArgumentsList = array + * @psalm-type QueryArgumentsList = array * @psalm-type CookiesList = array * @psalm-type UploadedFilesList = array + * + * @psalm-immutable */ #[Immutable] final class Request @@ -36,59 +38,26 @@ final class Request public const PARSED_BODY_ATTRIBUTE_NAME = 'rr_parsed_body'; /** - * @var string - */ - public string $remoteAddr = '127.0.0.1'; - - /** - * @var string - */ - public string $protocol = 'HTTP/1.0'; - - /** - * @var string - */ - public string $method = 'GET'; - - /** - * @var string - */ - public string $uri = 'http://localhost'; - - /** - * @var HeadersList - */ - public array $headers = []; - - /** - * @var CookiesList + * @param HeadersList $headers + * @param CookiesList $cookies + * @param UploadedFilesList $uploads + * @param AttributesList $attributes + * @param QueryArgumentsList $query */ - public array $cookies = []; - - /** - * @var UploadedFilesList - */ - public array $uploads = []; - - /** - * @var AttributesList - */ - public array $attributes = []; - - /** - * @var QueryArgumentsList - */ - public array $query = []; - - /** - * @var string - */ - public string $body = ''; - - /** - * @var bool - */ - public bool $parsed = false; + public function __construct( + public readonly string $remoteAddr = '127.0.0.1', + public readonly string $protocol = 'HTTP/1.0', + public readonly string $method = 'GET', + public readonly string $uri = 'http://localhost', + public readonly array $headers = [], + public readonly array $cookies = [], + public readonly array $uploads = [], + public readonly array $attributes = [], + public readonly array $query = [], + public readonly string $body = '', + public readonly bool $parsed = false, + ) { + } /** * @return string diff --git a/tests/Feature/StreamResponseTest.php b/tests/Feature/StreamResponseTest.php index afb5928..5c8cb2c 100644 --- a/tests/Feature/StreamResponseTest.php +++ b/tests/Feature/StreamResponseTest.php @@ -55,7 +55,7 @@ public function testStreamResponseWithMultipleFrames(): void $chunks = ['Hel', 'lo,', ' Wo', 'rld', '!']; ServerRunner::getBuffer(); - $httpWorker->respondStream( + $httpWorker->respond( 200, (function () use ($chunks) { yield from $chunks; @@ -73,7 +73,7 @@ public function testStopStreamResponse(): void // Flush buffer ServerRunner::getBuffer(); - $httpWorker->respondStream( + $httpWorker->respond( 200, (function () { yield 'Hel'; @@ -114,4 +114,4 @@ private function sendCommand(BaseCommand $command) $this->getRelay()->send($command->getRequestFrame()); \usleep(500_000); } -} \ No newline at end of file +} diff --git a/tests/Unit/StreamResponseTest.php b/tests/Unit/StreamResponseTest.php index 9113709..e491129 100644 --- a/tests/Unit/StreamResponseTest.php +++ b/tests/Unit/StreamResponseTest.php @@ -42,7 +42,7 @@ public function testStreamResponseWithMultipleFrames(): void { $httpWorker = $this->makeHttpWorker(); - $httpWorker->respondStream(200, (function () { + $httpWorker->respond(200, (function () { yield 'Hel'; yield 'lo,'; yield ' Wo'; @@ -58,7 +58,7 @@ public function testStopStreamResponse(): void { $httpWorker = $this->makeHttpWorker(); - $httpWorker->respondStream(200, (function () { + $httpWorker->respond(200, (function () { yield 'Hel'; yield 'lo,'; $this->getRelay()->addStopStreamFrame(); @@ -88,4 +88,4 @@ private function makeHttpWorker(): HttpWorker { return new HttpWorker($this->getWorker()); } -} \ No newline at end of file +} From fb7e0c80d1b4fa247b30dd24e5e1bcf70b9bbf98 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 30 Mar 2023 00:43:48 +0400 Subject: [PATCH 5/7] Update spiral/roadrunner-worker dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5c5e075..37982d8 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "psr/http-factory": "^1.0.1", "psr/http-message": "^1.0.1", "spiral/roadrunner": "^2023.1", - "spiral/roadrunner-worker": "^2.2.0" + "spiral/roadrunner-worker": "^3.0" }, "require-dev": { "nyholm/psr7": "^1.3", From 28e6fcdfc1b2f2ad061652ef0089405f4c87807e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 12 Apr 2023 11:18:46 +0400 Subject: [PATCH 6/7] Fix psalm issues; update composer.json; migrate phpunit config --- composer.json | 35 +++++++++++++++++++++++++--- phpunit.xml | 10 ++------ src/HttpWorker.php | 8 +++---- src/Request.php | 6 ++--- tests/Feature/StreamResponseTest.php | 29 ++++++++++++++++++++++- 5 files changed, 69 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index 37982d8..a78c314 100644 --- a/composer.json +++ b/composer.json @@ -5,14 +5,37 @@ "license": "MIT", "authors": [ { - "name": "Anton Titov / Wolfy-J", + "name": "Anton Titov (Wolfy-J)", "email": "wolfy.jd@gmail.com" }, + { + "name": "Valery Piashchynski", + "homepage": "https://github.com/rustatian" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Pavel Buchnev (butschster)", + "email": "pavel.buchnev@spiralscout.com" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + }, { "name": "RoadRunner Community", - "homepage": "https://github.com/spiral/roadrunner/graphs/contributors" + "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" } ], + "homepage": "https://spiral.dev/", + "support": { + "docs": "https://roadrunner.dev/docs", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "forum": "https://forum.roadrunner.dev/", + "chat": "https://discord.gg/V6EK4he" + }, "require": { "php": ">=8.1", "ext-json": "*", @@ -26,7 +49,7 @@ "phpunit/phpunit": "^10.0", "jetbrains/phpstorm-attributes": "^1.0", "symfony/process": "^6.2", - "vimeo/psalm": "^5.8" + "vimeo/psalm": "^5.9" }, "autoload": { "psr-4": { @@ -38,6 +61,12 @@ "Spiral\\RoadRunner\\Tests\\Http\\": "tests" } }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/roadrunner-server" + } + ], "scripts": { "analyze": "psalm" }, diff --git a/phpunit.xml b/phpunit.xml index bc5538c..c12c7b2 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,25 +1,19 @@ tests - src diff --git a/src/HttpWorker.php b/src/HttpWorker.php index 24b746f..384cf56 100644 --- a/src/HttpWorker.php +++ b/src/HttpWorker.php @@ -22,10 +22,10 @@ * @psalm-import-type UploadedFilesList from Request * @psalm-import-type CookiesList from Request * - * @psalm-type RequestContext = array { - * remoteAddr: string, - * protocol: string, - * method: string, + * @psalm-type RequestContext = array{ + * remoteAddr: non-empty-string, + * protocol: non-empty-string, + * method: non-empty-string, * uri: string, * attributes: AttributesList, * headers: HeadersList, diff --git a/src/Request.php b/src/Request.php index 8b38af8..881dde5 100644 --- a/src/Request.php +++ b/src/Request.php @@ -16,10 +16,10 @@ /** * @psalm-immutable * - * @psalm-type UploadedFile = array { - * name: string, + * @psalm-type UploadedFile = array{ + * name: non-empty-string, * error: int<0, max>, - * tmpName: string, + * tmpName: non-empty-string, * size: int<0, max>, * mime: string * } diff --git a/tests/Feature/StreamResponseTest.php b/tests/Feature/StreamResponseTest.php index 5c8cb2c..08fda84 100644 --- a/tests/Feature/StreamResponseTest.php +++ b/tests/Feature/StreamResponseTest.php @@ -5,7 +5,6 @@ namespace Spiral\RoadRunner\Tests\Http\Feature; use PHPUnit\Framework\TestCase; -use Spiral\Goridge\Frame; use Spiral\Goridge\SocketRelay; use Spiral\RoadRunner\Http\HttpWorker; use Spiral\RoadRunner\Payload; @@ -94,6 +93,34 @@ public function testStopStreamResponse(): void self::assertSame(\implode("\n", ['Hel', 'lo,']), \trim(ServerRunner::getBuffer())); } + /** + * StopStream should be ignored if stream is already ended. + */ + public function testStopStreamAfterStreamEnd(): void + { + $httpWorker = $this->makeHttpWorker(); + + // Flush buffer + ServerRunner::getBuffer(); + + $httpWorker->respond( + 200, + (function () { + yield 'Hello'; + yield 'World!'; + })(), + ); + + $this->assertFalse($this->getWorker()->hasPayload(\Spiral\RoadRunner\Message\Command\StreamStop::class)); + $this->sendCommand(new StreamStop()); + + + \usleep(200_000); + self::assertSame(\implode("\n", ['Hello', 'World!']), \trim(ServerRunner::getBuffer())); + $this->assertTrue($this->getWorker()->hasPayload(\Spiral\RoadRunner\Message\Command\StreamStop::class)); + $this->assertFalse($this->getWorker()->hasPayload()); + } + private function getRelay(): SocketRelay { return $this->relay ??= SocketRelay::create($this->serverAddress); From 529f9621cd6e74088fbede8358b6b59981ad8cea Mon Sep 17 00:00:00 2001 From: Pavel Buchnev Date: Thu, 13 Apr 2023 15:13:49 +0400 Subject: [PATCH 7/7] Cleanup --- .github/FUNDING.yml | 3 ++ .github/workflows/phpunit.yml | 4 +- .github/workflows/psalm.yml | 2 +- .gitignore | 3 ++ composer.json | 6 +-- src/HttpWorker.php | 15 ++----- src/HttpWorkerInterface.php | 9 ---- src/PSR7Worker.php | 67 +++------------------------- src/PSR7WorkerInterface.php | 7 --- src/Request.php | 11 ----- tests/Feature/StreamResponseTest.php | 4 +- 11 files changed, 23 insertions(+), 108 deletions(-) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..38798a2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: roadrunner-server diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 854efaa..504edfd 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -1,9 +1,9 @@ on: + pull_request: null push: branches: - master - '*.*' - pull_request: null name: phpunit @@ -16,4 +16,4 @@ jobs: php: >- ['8.1', '8.2'] stability: >- - ['prefer-stable', 'prefer-lowest'] + ['prefer-lowest', 'prefer-stable'] diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index bdb570c..02a83d3 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -1,9 +1,9 @@ on: + pull_request: null push: branches: - master - '*.*' - pull_request: null name: static analysis diff --git a/.gitignore b/.gitignore index 4c64f4e..4059830 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ clover* cover* .DS_Store *.cache + +.phpunit.cache/ +.phpunit.result.cache diff --git a/composer.json b/composer.json index a78c314..6402581 100644 --- a/composer.json +++ b/composer.json @@ -5,8 +5,8 @@ "license": "MIT", "authors": [ { - "name": "Anton Titov (Wolfy-J)", - "email": "wolfy.jd@gmail.com" + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" }, { "name": "Valery Piashchynski", @@ -45,9 +45,9 @@ "spiral/roadrunner-worker": "^3.0" }, "require-dev": { + "jetbrains/phpstorm-attributes": "^1.0", "nyholm/psr7": "^1.3", "phpunit/phpunit": "^10.0", - "jetbrains/phpstorm-attributes": "^1.0", "symfony/process": "^6.2", "vimeo/psalm": "^5.9" }, diff --git a/src/HttpWorker.php b/src/HttpWorker.php index 384cf56..d07b0ea 100644 --- a/src/HttpWorker.php +++ b/src/HttpWorker.php @@ -1,12 +1,5 @@ $status, 'headers' => $headers ?: (object)[], ], \JSON_THROW_ON_ERROR); @@ -87,7 +80,7 @@ public function respond(int $status, string|Generator $body, array $headers = [] private function respondStream(int $status, Generator $body, array $headers = []): void { - $head = (string)\json_encode([ + $head = \json_encode([ 'status' => $status, 'headers' => $headers ?: (object)[], ], \JSON_THROW_ON_ERROR); @@ -124,11 +117,11 @@ private function createRequest(string $body, array $context): Request cookies: (array)($context['cookies'] ?? []), uploads: (array)($context['uploads'] ?? []), attributes: [ - Request::PARSED_BODY_ATTRIBUTE_NAME => (bool)$context['parsed'], + Request::PARSED_BODY_ATTRIBUTE_NAME => $context['parsed'], ] + (array)($context['attributes'] ?? []), query: $query, body: $body, - parsed: (bool)$context['parsed'], + parsed: $context['parsed'], ); } diff --git a/src/HttpWorkerInterface.php b/src/HttpWorkerInterface.php index 8eef4d1..9198e7b 100644 --- a/src/HttpWorkerInterface.php +++ b/src/HttpWorkerInterface.php @@ -1,12 +1,5 @@ httpWorker = new HttpWorker($worker); - $this->requestFactory = $requestFactory; - $this->streamFactory = $streamFactory; - $this->uploadsFactory = $uploadsFactory; $this->originalServer = $_SERVER; } - /** - * @return WorkerInterface - */ public function getWorker(): WorkerInterface { return $this->httpWorker->getWorker(); } /** - * @return ServerRequestInterface|null * @throws \JsonException */ public function waitRequest(): ?ServerRequestInterface @@ -113,7 +72,6 @@ public function waitRequest(): ?ServerRequestInterface /** * Send response to the application server. * - * @param ResponseInterface $response * @throws \JsonException */ public function respond(ResponseInterface $response): void @@ -158,7 +116,6 @@ private function streamToGenerator(StreamInterface $stream): Generator * Returns altered copy of _SERVER variable. Sets ip-address, * request-time and other values. * - * @param Request $request * @return non-empty-array */ protected function configureServer(Request $request): array @@ -173,7 +130,7 @@ protected function configureServer(Request $request): array $server['HTTP_USER_AGENT'] = ''; foreach ($request->headers as $key => $value) { - $key = \strtoupper(\str_replace('-', '_', (string)$key)); + $key = \strtoupper(\str_replace('-', '_', $key)); if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) { $server[$key] = \implode(', ', $value); } else { @@ -184,26 +141,17 @@ protected function configureServer(Request $request): array return $server; } - /** - * @return int - */ protected function timeInt(): int { return \time(); } - /** - * @return float - */ protected function timeFloat(): float { return \microtime(true); } /** - * @param Request $httpRequest - * @param array $server - * @return ServerRequestInterface * @throws \JsonException */ protected function mapRequest(Request $httpRequest, array $server): ServerRequestInterface @@ -278,9 +226,6 @@ protected function wrapUploads(array $files): array /** * Normalize HTTP protocol version to valid values - * - * @param string $version - * @return string */ private static function fetchProtocolVersion(string $version): string { diff --git a/src/PSR7WorkerInterface.php b/src/PSR7WorkerInterface.php index dbb710e..becbfb2 100644 --- a/src/PSR7WorkerInterface.php +++ b/src/PSR7WorkerInterface.php @@ -1,12 +1,5 @@ attributes['ipAddress'] ?? $this->remoteAddr); } /** - * @return array|null * @throws \JsonException */ public function getParsedBody(): ?array diff --git a/tests/Feature/StreamResponseTest.php b/tests/Feature/StreamResponseTest.php index 08fda84..ca8c370 100644 --- a/tests/Feature/StreamResponseTest.php +++ b/tests/Feature/StreamResponseTest.php @@ -113,8 +113,6 @@ public function testStopStreamAfterStreamEnd(): void $this->assertFalse($this->getWorker()->hasPayload(\Spiral\RoadRunner\Message\Command\StreamStop::class)); $this->sendCommand(new StreamStop()); - - \usleep(200_000); self::assertSame(\implode("\n", ['Hello', 'World!']), \trim(ServerRunner::getBuffer())); $this->assertTrue($this->getWorker()->hasPayload(\Spiral\RoadRunner\Message\Command\StreamStop::class)); @@ -128,7 +126,7 @@ private function getRelay(): SocketRelay private function getWorker(): Worker { - return $this->worker ??= new Worker($this->getRelay(), false); + return $this->worker ??= new Worker(relay: $this->getRelay(), interceptSideEffects: false); } private function makeHttpWorker(): HttpWorker