From e62ed65c94bd7986999c35ffa5893fbed30b686c Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 31 Aug 2025 16:27:52 +0400 Subject: [PATCH 1/4] Configure code style --- .github/workflows/cs-fix.yml | 12 ++++++++++++ .php-cs-fixer.dist.php | 12 ++++++++++++ composer.json | 17 +++++++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/cs-fix.yml create mode 100644 .php-cs-fixer.dist.php diff --git a/.github/workflows/cs-fix.yml b/.github/workflows/cs-fix.yml new file mode 100644 index 0000000..0395b27 --- /dev/null +++ b/.github/workflows/cs-fix.yml @@ -0,0 +1,12 @@ +on: + push: + branches: + - '*' + +name: Fix Code Style + +jobs: + cs-fix: + permissions: + contents: write + uses: spiral/gh-actions/.github/workflows/cs-fix.yml@master diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..7b5866c --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,12 @@ +include(__DIR__ . '/src') + ->include(__DIR__ . '/tests') + ->include(__FILE__) + ->allowRisky(false) + ->build(); diff --git a/composer.json b/composer.json index 2459f2d..f87a829 100644 --- a/composer.json +++ b/composer.json @@ -50,6 +50,7 @@ "jetbrains/phpstorm-attributes": "^1.0", "nyholm/psr7": "^1.3", "phpunit/phpunit": "^10.0", + "spiral/code-style": "^2.3", "spiral/dumper": "^3.3", "symfony/process": "^6.2 || ^7.0", "vimeo/psalm": "^5.9" @@ -70,9 +71,6 @@ "url": "https://github.com/sponsors/roadrunner-server" } ], - "scripts": { - "analyze": "psalm" - }, "suggest": { "spiral/roadrunner-cli": "Provides RoadRunner installation and management CLI tools", "ext-protobuf": "Provides Protocol Buffers support. Without it, performance will be lower." @@ -81,5 +79,16 @@ "sort-packages": true }, "minimum-stability": "dev", - "prefer-stable": true + "prefer-stable": true, + "scripts": { + "cs:fix": "php-cs-fixer fix -v", + "psalm": "psalm", + "psalm:baseline": "psalm --set-baseline=psalm-baseline.xml", + "psalm:ci": "psalm --output-format=github --shepherd --show-info=false --stats --threads=4", + "test": "phpunit --color=always --testdox", + "test:cc": [ + "@putenv XDEBUG_MODE=coverage", + "phpunit --coverage-clover=runtime/phpunit/logs/clover.xml --color=always" + ] + } } From 5245bfdece6e5d360a35eca06b660c4dcf0a6aa8 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 31 Aug 2025 16:34:03 +0400 Subject: [PATCH 2/4] Apply code style --- src/GlobalState.php | 16 +- src/HttpWorker.php | 27 ++- src/HttpWorkerInterface.php | 5 +- src/PSR7Worker.php | 67 ++++--- src/Request.php | 7 +- tests/Feature/StreamResponseTest.php | 31 ++- tests/Server/Client.php | 18 +- tests/Server/Command/BaseCommand.php | 8 +- tests/Server/Server.php | 22 +-- tests/Server/ServerRunner.php | 9 +- tests/Unit/HttpWorkerTest.php | 275 +++++++++++++-------------- tests/Unit/PSR7WorkerTest.php | 12 +- tests/Unit/StreamResponseTest.php | 10 +- tests/Unit/Stub/TestRelay.php | 6 +- 14 files changed, 245 insertions(+), 268 deletions(-) diff --git a/src/GlobalState.php b/src/GlobalState.php index 04f87ed..9303149 100644 --- a/src/GlobalState.php +++ b/src/GlobalState.php @@ -4,12 +4,6 @@ namespace Spiral\RoadRunner\Http; -use function time; -use function microtime; -use function strtoupper; -use function str_replace; -use function implode; - final class GlobalState { /** @@ -35,22 +29,22 @@ public static function enrichServerVars(Request $request): array $server = self::$cachedServer; $server['REQUEST_URI'] = $request->uri; - $server['REQUEST_TIME'] = time(); - $server['REQUEST_TIME_FLOAT'] = microtime(true); + $server['REQUEST_TIME'] = \time(); + $server['REQUEST_TIME_FLOAT'] = \microtime(true); $server['REMOTE_ADDR'] = $request->getRemoteAddr(); $server['REQUEST_METHOD'] = $request->method; $server['HTTP_USER_AGENT'] = ''; foreach ($request->headers as $key => $value) { - $key = strtoupper(str_replace('-', '_', $key)); + $key = \strtoupper(\str_replace('-', '_', $key)); if ($key == 'CONTENT_TYPE' || $key == 'CONTENT_LENGTH') { - $server[$key] = implode(', ', $value); + $server[$key] = \implode(', ', $value); continue; } - $server['HTTP_' . $key] = implode(', ', $value); + $server['HTTP_' . $key] = \implode(', ', $value); } return $server; diff --git a/src/HttpWorker.php b/src/HttpWorker.php index e82c1bb..d22e63a 100644 --- a/src/HttpWorker.php +++ b/src/HttpWorker.php @@ -42,8 +42,7 @@ class HttpWorker implements HttpWorkerInterface public function __construct( private readonly WorkerInterface $worker, - ) { - } + ) {} public function getWorker(): WorkerInterface { @@ -83,13 +82,13 @@ public function waitRequest(): ?Request * @param array> $headers * @throws \JsonException */ - public function respond(int $status, string|Generator $body = '', array $headers = [], bool $endOfStream = true): void + public function respond(int $status, string|\Generator $body = '', array $headers = [], bool $endOfStream = true): void { if ($status < 200 && $status >= 100 && $body !== '') { throw new \InvalidArgumentException('Unable to send a body with informational status code.'); } - if ($body instanceof Generator) { + if ($body instanceof \Generator) { $this->respondStream($status, $body, $headers, $endOfStream); return; } @@ -101,7 +100,7 @@ public function respond(int $status, string|Generator $body = '', array $headers /** * @param array> $headers */ - private function respondStream(int $status, Generator $body, array $headers = [], bool $endOfStream = true): void + private function respondStream(int $status, \Generator $body, array $headers = [], bool $endOfStream = true): void { $worker = $this->worker instanceof StreamWorkerInterface ? $this->worker->withStreamMode() @@ -110,7 +109,7 @@ private function respondStream(int $status, Generator $body, array $headers = [] do { if (!$body->valid()) { // End of generator - $content = (string)$body->getReturn(); + $content = (string) $body->getReturn(); if ($endOfStream === false && $content === '') { // We don't need to send an empty frame if the stream is not ended return; @@ -118,12 +117,12 @@ private function respondStream(int $status, Generator $body, array $headers = [] /** @psalm-suppress TooManyArguments */ $worker->respond( $this->createRespondPayload($status, $content, $headers, $endOfStream), - static::$codec + static::$codec, ); break; } - $content = (string)$body->current(); + $content = (string) $body->current(); if ($worker->getPayload(StreamStop::class) !== null) { $body->throw(new StreamStoppedException()); @@ -160,12 +159,12 @@ private function arrayToRequest(string $body, array $context): Request protocol: $context['protocol'], method: $context['method'], uri: $context['uri'], - headers: $this->filterHeaders((array)($context['headers'] ?? [])), - cookies: (array)($context['cookies'] ?? []), - uploads: (array)($context['uploads'] ?? []), + headers: $this->filterHeaders((array) ($context['headers'] ?? [])), + cookies: (array) ($context['cookies'] ?? []), + uploads: (array) ($context['uploads'] ?? []), attributes: [ Request::PARSED_BODY_ATTRIBUTE_NAME => $context['parsed'], - ] + (array)($context['attributes'] ?? []), + ] + (array) ($context['attributes'] ?? []), query: $query, body: $body, parsed: $context['parsed'], @@ -253,7 +252,7 @@ private function arrayToHeaderValue(array $headers = []): array */ foreach ($headers as $key => $value) { /** @psalm-suppress DocblockTypeContradiction */ - $value = \array_filter(\is_array($value) ? $value : [$value], static fn (mixed $v): bool => \is_string($v)); + $value = \array_filter(\is_array($value) ? $value : [$value], static fn(mixed $v): bool => \is_string($v)); if ($value !== []) { $result[$key] = new HeaderValue(['value' => $value]); } @@ -270,7 +269,7 @@ private function createRespondPayload(int $status, string $body, array $headers $head = static::$codec === Frame::CODEC_PROTO ? (new Response(['status' => $status, 'headers' => $this->arrayToHeaderValue($headers)])) ->serializeToString() - : \json_encode(['status' => $status, 'headers' => $headers ?: (object)[]], \JSON_THROW_ON_ERROR); + : \json_encode(['status' => $status, 'headers' => $headers ?: (object) []], \JSON_THROW_ON_ERROR); return new Payload(body: $body, header: $head, eos: $eos); } diff --git a/src/HttpWorkerInterface.php b/src/HttpWorkerInterface.php index 8643e77..3531765 100644 --- a/src/HttpWorkerInterface.php +++ b/src/HttpWorkerInterface.php @@ -6,7 +6,6 @@ use Generator; use Spiral\RoadRunner\WorkerAwareInterface; -use Stringable; /** * @psalm-import-type HeadersList from Request @@ -22,7 +21,7 @@ public function waitRequest(): ?Request; * Send response to the application server. * * @param int $status Http status code - * @param Generator|string $body Body of response. + * @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 supported by RoadRunner since version 2023.3 @@ -30,5 +29,5 @@ public function waitRequest(): ?Request; * 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|Generator $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 43569b6..02e04f4 100644 --- a/src/PSR7Worker.php +++ b/src/PSR7Worker.php @@ -4,7 +4,6 @@ namespace Spiral\RoadRunner\Http; -use Generator; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestFactoryInterface; use Psr\Http\Message\ServerRequestInterface; @@ -13,7 +12,6 @@ use Psr\Http\Message\UploadedFileFactoryInterface; use Psr\Http\Message\UploadedFileInterface; use Spiral\RoadRunner\WorkerInterface; -use Stringable; /** * Manages PSR-7 request and response. @@ -32,7 +30,6 @@ class PSR7Worker implements PSR7WorkerInterface private readonly HttpWorker $httpWorker; - /** * @var string[] Valid values for HTTP protocol version */ @@ -91,38 +88,11 @@ public function respond(ResponseInterface $response): void $response->getStatusCode(), $this->chunkSize > 0 ? $this->streamToGenerator($response->getBody()) - : (string)$response->getBody(), - $response->getHeaders() + : (string) $response->getBody(), + $response->getHeaders(), ); } - /** - * @return Generator Compatible - * with {@see HttpWorker::respondStream}. - */ - private function streamToGenerator(StreamInterface $stream): Generator - { - $stream->rewind(); - $size = $stream->getSize(); - if ($size !== null && $size < $this->chunkSize) { - return (string)$stream; - } - $sum = 0; - while (!$stream->eof()) { - if ($size === null) { - $chunk = $stream->read($this->chunkSize); - } else { - $left = $size - $sum; - $chunk = $stream->read(\min($this->chunkSize, $left)); - if ($left <= $this->chunkSize && \strlen($chunk) === $left) { - return $chunk; - } - } - $sum += \strlen($chunk); - yield $chunk; - } - } - /** * Returns altered copy of _SERVER variable. Sets ip-address, * request-time and other values. @@ -158,7 +128,7 @@ protected function mapRequest(Request $httpRequest, array $server): ServerReques $request = $this->requestFactory->createServerRequest( $httpRequest->method, $httpRequest->uri, - $server + $server, ); $request = $request @@ -205,7 +175,7 @@ protected function wrapUploads(array $files): array continue; } - if (\UPLOAD_ERR_OK === $file['error']) { + if ($file['error'] === \UPLOAD_ERR_OK) { $stream = $this->streamFactory->createStreamFromFile($file['tmpName']); } else { $stream = $this->streamFactory->createStream(); @@ -216,7 +186,7 @@ protected function wrapUploads(array $files): array $file['size'], $file['error'], $file['name'], - $file['mime'] + $file['mime'], ); } @@ -241,4 +211,31 @@ private static function fetchProtocolVersion(string $version): string return $v; } + + /** + * @return \Generator Compatible + * with {@see HttpWorker::respondStream}. + */ + private function streamToGenerator(StreamInterface $stream): \Generator + { + $stream->rewind(); + $size = $stream->getSize(); + if ($size !== null && $size < $this->chunkSize) { + return (string) $stream; + } + $sum = 0; + while (!$stream->eof()) { + if ($size === null) { + $chunk = $stream->read($this->chunkSize); + } else { + $left = $size - $sum; + $chunk = $stream->read(\min($this->chunkSize, $left)); + if ($left <= $this->chunkSize && \strlen($chunk) === $left) { + return $chunk; + } + } + $sum += \strlen($chunk); + yield $chunk; + } + } } diff --git a/src/Request.php b/src/Request.php index dbe17ce..b40c4c8 100644 --- a/src/Request.php +++ b/src/Request.php @@ -49,12 +49,11 @@ public function __construct( public readonly array $query = [], public readonly string $body = '', public readonly bool $parsed = false, - ) { - } + ) {} public function getRemoteAddr(): string { - return (string)($this->attributes['ipAddress'] ?? $this->remoteAddr); + return (string) ($this->attributes['ipAddress'] ?? $this->remoteAddr); } /** @@ -63,7 +62,7 @@ public function getRemoteAddr(): string public function getParsedBody(): ?array { if ($this->parsed) { - return (array)\json_decode($this->body, true, 512, \JSON_THROW_ON_ERROR); + return (array) \json_decode($this->body, true, 512, \JSON_THROW_ON_ERROR); } return null; diff --git a/tests/Feature/StreamResponseTest.php b/tests/Feature/StreamResponseTest.php index 9da68b8..5c5b9e8 100644 --- a/tests/Feature/StreamResponseTest.php +++ b/tests/Feature/StreamResponseTest.php @@ -4,7 +4,6 @@ namespace Spiral\RoadRunner\Tests\Http\Feature; -use Exception; use PHPUnit\Framework\TestCase; use Spiral\Goridge\SocketRelay; use Spiral\RoadRunner\Http\Exception\StreamStoppedException; @@ -22,20 +21,6 @@ class StreamResponseTest extends TestCase private Worker $worker; private $serverAddress = 'tcp://127.0.0.1:6002'; - protected function setUp(): void - { - parent::setUp(); - ServerRunner::start(); - ServerRunner::getBuffer(); - } - - protected function tearDown(): void - { - unset($this->relay, $this->worker); - ServerRunner::stop(); - parent::tearDown(); - } - /** * Regular case */ @@ -123,7 +108,7 @@ public function testExceptionInGenerator(): void (function () { yield 'Hel'; yield 'lo,'; - throw new Exception('test'); + throw new \Exception('test'); })(), ); @@ -163,6 +148,20 @@ public function testStopStreamAfterStreamEnd(): void $this->assertFalse($this->getWorker()->hasPayload()); } + protected function setUp(): void + { + parent::setUp(); + ServerRunner::start(); + ServerRunner::getBuffer(); + } + + protected function tearDown(): void + { + unset($this->relay, $this->worker); + ServerRunner::stop(); + parent::tearDown(); + } + private function getRelay(): SocketRelay { return $this->relay ??= SocketRelay::create($this->serverAddress); diff --git a/tests/Server/Client.php b/tests/Server/Client.php index e5dc0c7..1afd62f 100644 --- a/tests/Server/Client.php +++ b/tests/Server/Client.php @@ -4,7 +4,6 @@ namespace Spiral\RoadRunner\Tests\Http\Server; -use Fiber; use Spiral\Goridge\Frame; use Spiral\RoadRunner\Tests\Http\Server\Command\BaseCommand; use Spiral\RoadRunner\Tests\Http\Server\Command\StreamStop; @@ -19,7 +18,6 @@ class Client /** @var string[] */ private array $writeQueue = []; - /** @var string */ private string $readBuffer = ''; public function __construct( @@ -29,11 +27,6 @@ public function __construct( \socket_set_nonblock($this->socket); } - public function __destruct() - { - \socket_close($this->socket); - } - public static function init(\Socket $socket): self { return new self($socket); @@ -59,10 +52,15 @@ public function process(): void $this->writeQueue(); } - Fiber::suspend(); + \Fiber::suspend(); } while (true); } + public function __destruct() + { + \socket_close($this->socket); + } + private function onInit() { $this->writeQueue[] = Frame::packFrame(new Frame('{"pid":true}', [], Frame::CONTROL)); @@ -130,7 +128,7 @@ private function readNBytes(int $bytes, bool $canBeLess = false): string } if ($data === '') { - Fiber::suspend(); + \Fiber::suspend(); continue; } @@ -170,4 +168,4 @@ private function onCommand(BaseCommand $command): void break; } } -} \ No newline at end of file +} diff --git a/tests/Server/Command/BaseCommand.php b/tests/Server/Command/BaseCommand.php index f36e7fd..8a2a843 100644 --- a/tests/Server/Command/BaseCommand.php +++ b/tests/Server/Command/BaseCommand.php @@ -9,9 +9,11 @@ abstract class BaseCommand { public const COMMAND_KEY = 'test-command'; + protected Frame $frame; - public function __construct() { + public function __construct() + { $this->frame = new Frame(\json_encode([self::COMMAND_KEY => static::class])); } @@ -25,5 +27,5 @@ public function getResponse(): string return Frame::packFrame($this->getResponseFrame()); } - public abstract function getResponseFrame(): Frame; -} \ No newline at end of file + abstract public function getResponseFrame(): Frame; +} diff --git a/tests/Server/Server.php b/tests/Server/Server.php index 2e1f865..4b7c85b 100644 --- a/tests/Server/Server.php +++ b/tests/Server/Server.php @@ -4,19 +4,15 @@ namespace Spiral\RoadRunner\Tests\Http\Server; -use Fiber; -use RuntimeException; -use Socket; - class Server { - /** @var false|resource|Socket */ + /** @var false|resource|\Socket */ private $socket; /** @var Client[] */ private array $clients = []; - /** @var Fiber[] */ + /** @var \Fiber[] */ private array $fibers = []; public function __construct( @@ -31,11 +27,6 @@ public function __construct( echo "Server started\n"; } - public function __destruct() - { - \socket_close($this->socket); - } - public static function init(int $port = 6002): self { return new self($port); @@ -48,7 +39,7 @@ public function process(): void $key = \array_key_last($this->clients) + 1; try { $this->clients[$key] = Client::init($client); - $this->fibers[$key] = new Fiber($this->clients[$key]->process(...)); + $this->fibers[$key] = new \Fiber($this->clients[$key]->process(...)); } catch (\Throwable) { unset($this->clients[$key], $this->fibers[$key]); } @@ -59,11 +50,16 @@ public function process(): void $fiber->isStarted() ? $fiber->resume() : $fiber->start(); if ($fiber->isTerminated()) { - throw new RuntimeException('Client terminated.'); + throw new \RuntimeException('Client terminated.'); } } catch (\Throwable) { unset($this->clients[$key], $this->fibers[$key]); } } } + + public function __destruct() + { + \socket_close($this->socket); + } } diff --git a/tests/Server/ServerRunner.php b/tests/Server/ServerRunner.php index 0ba49f8..9bb8b8a 100644 --- a/tests/Server/ServerRunner.php +++ b/tests/Server/ServerRunner.php @@ -4,9 +4,6 @@ namespace Spiral\RoadRunner\Tests\Http\Server; - -use RuntimeException; -use Symfony\Component\Process\PhpProcess; use Symfony\Component\Process\Process; class ServerRunner @@ -19,7 +16,7 @@ public static function start(int $timeout = 5): void self::$process = new Process(['php', 'run_server.php'], __DIR__); $run = false; self::$process->setTimeout($timeout); - self::$process->start(static function (string $type, string $output) use (&$run) { + self::$process->start(static function (string $type, string $output) use (&$run) { if (!$run && $type === Process::OUT && \str_contains($output, 'Server started')) { $run = true; } @@ -30,7 +27,7 @@ public static function start(int $timeout = 5): void }); if (!self::$process->isRunning()) { - throw new RuntimeException('Error starting Server: ' . self::$process->getErrorOutput()); + throw new \RuntimeException('Error starting Server: ' . self::$process->getErrorOutput()); } // wait for roadrunner to start @@ -42,7 +39,7 @@ public static function start(int $timeout = 5): void } if (!$run) { - throw new RuntimeException('Error starting Server: timeout'); + throw new \RuntimeException('Error starting Server: timeout'); } } diff --git a/tests/Unit/HttpWorkerTest.php b/tests/Unit/HttpWorkerTest.php index 6a094c2..ec1b44c 100644 --- a/tests/Unit/HttpWorkerTest.php +++ b/tests/Unit/HttpWorkerTest.php @@ -24,7 +24,6 @@ final class HttpWorkerTest extends TestCase 'uri' => 'http://localhost', 'parsed' => false, ]; - private const REQUIRED_REQUEST_DATA = [ 'remoteAddr' => '127.0.0.1', 'protocol' => 'HTTP/1.1', @@ -33,124 +32,9 @@ final class HttpWorkerTest extends TestCase 'attributes' => [Request::PARSED_BODY_ATTRIBUTE_NAME => false], 'query' => ['first' => 'value', 'arr' => ['foo bar', 'baz']], 'parsed' => false, - 'body' => 'foo' + 'body' => 'foo', ]; - #[DataProvider('requestDataProvider')] - public function testWaitRequestFromArray(array $header, array $expected): void - { - $worker = $this->createMock(WorkerInterface::class); - $worker->expects($this->once()) - ->method('waitPayload') - ->willReturn(new Payload('foo', \json_encode($header))); - - $worker = new HttpWorker($worker); - - $this->assertEquals(new Request(...$expected), $worker->waitRequest()); - } - - #[DataProvider('requestDataProvider')] - public function testWaitRequestFromProto(array $header, array $expected): void - { - $request = self::createProtoRequest($header); - - $worker = $this->createMock(WorkerInterface::class); - $worker->expects($this->once()) - ->method('waitPayload') - ->willReturn(new Payload('foo', $request->serializeToString())); - - $worker = new HttpWorker($worker); - - $this->assertEquals(new Request(...$expected), $worker->waitRequest()); - } - - #[DataProvider('emptyRequestDataProvider')] - public function testWaitRequestWithEmptyData(?Payload $payload): void - { - $worker = $this->createMock(WorkerInterface::class); - $worker->expects($this->once()) - ->method('waitPayload') - ->willReturn($payload); - - $worker = new HttpWorker($worker); - - $this->assertEquals(null, $worker->waitRequest()); - } - - public function testEmptyBodyShouldBeConvertedIntoEmptyArrayWithParsedTrue(): void - { - $request = self::createProtoRequest(\array_merge(self::REQUIRED_REQUEST_DATA, ['parsed' => true])); - - $worker = $this->createMock(WorkerInterface::class); - $worker->expects($this->once()) - ->method('waitPayload') - ->willReturn(new Payload('', $request->serializeToString())); - - $worker = new HttpWorker($worker); - - $request = $worker->waitRequest(); - $this->assertSame([], $request->getParsedBody()); - } - - public function testRespondUnableToSendBodyWithInfoStatusException(): void - { - $worker = new HttpWorker($this->createMock(WorkerInterface::class)); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Unable to send a body with informational status code.'); - $worker->respond(100, 'foo'); - } - - public function testRespondWithProtoCodec(): void - { - $expectedHeader = new Response([ - 'status' => 200, - 'headers' => ['Content-Type' => new HeaderValue(['value' => ['application/x-www-form-urlencoded']])], - ]); - - $worker = $this->createMock(WorkerInterface::class); - $worker->expects($this->once()) - ->method('respond') - ->with(new Payload('foo', $expectedHeader->serializeToString()), Frame::CODEC_PROTO); - - (new \ReflectionProperty(HttpWorker::class, 'codec'))->setValue(Frame::CODEC_PROTO); - $worker = new HttpWorker($worker); - - $worker->respond(200, 'foo', ['Content-Type' => ['application/x-www-form-urlencoded']]); - } - - #[DataProvider('headersDataProvider')] - public function testRespondWithProtoCodecWithHeaders(array $headers, array $expected): void - { - $expectedHeader = new Response(['status' => 200, 'headers' => $expected]); - - $worker = $this->createMock(WorkerInterface::class); - $worker->expects($this->once()) - ->method('respond') - ->with(new Payload('foo', $expectedHeader->serializeToString()), Frame::CODEC_PROTO); - - (new \ReflectionProperty(HttpWorker::class, 'codec'))->setValue(Frame::CODEC_PROTO); - $worker = new HttpWorker($worker); - - $worker->respond(200, 'foo', $headers); - } - - public function testRespondWithJsonCodec(): void - { - $worker = $this->createMock(WorkerInterface::class); - $worker->expects($this->once()) - ->method('respond') - ->with(new Payload('foo', \json_encode([ - 'status' => 200, - 'headers' => ['Content-Type' => ['application/x-www-form-urlencoded']] - ])), Frame::CODEC_JSON); - - (new \ReflectionProperty(HttpWorker::class, 'codec'))->setValue(Frame::CODEC_JSON); - $worker = new HttpWorker($worker); - - $worker->respond(200, 'foo', ['Content-Type' => ['application/x-www-form-urlencoded']]); - } - public static function requestDataProvider(): \Traversable { yield [self::REQUIRED_PAYLOAD_DATA, self::REQUIRED_REQUEST_DATA]; @@ -158,8 +42,8 @@ public static function requestDataProvider(): \Traversable \array_merge(self::REQUIRED_PAYLOAD_DATA, ['parsed' => true]), \array_merge( self::REQUIRED_REQUEST_DATA, - ['parsed' => true, 'attributes' => [Request::PARSED_BODY_ATTRIBUTE_NAME => true]] - ) + ['parsed' => true, 'attributes' => [Request::PARSED_BODY_ATTRIBUTE_NAME => true]], + ), ]; yield [ \array_merge(self::REQUIRED_PAYLOAD_DATA, [ @@ -171,7 +55,7 @@ public static function requestDataProvider(): \Traversable ]), \array_merge(self::REQUIRED_REQUEST_DATA, [ 'headers' => ['Content-Type' => ['application/x-www-form-urlencoded']], - ]) + ]), ]; yield [ \array_merge(self::REQUIRED_PAYLOAD_DATA, [ @@ -181,7 +65,7 @@ public static function requestDataProvider(): \Traversable ]), \array_merge(self::REQUIRED_REQUEST_DATA, [ 'cookies' => ['theme' => 'light'], - ]) + ]), ]; yield [ \array_merge(self::REQUIRED_PAYLOAD_DATA, [ @@ -207,7 +91,7 @@ public static function requestDataProvider(): \Traversable 'size' => 1235, 'error' => 0, 'tmpName' => '/tmp/php/php2h4j1o', - ] + ], ], 'nested' => [ 'some-key' => [ @@ -217,7 +101,7 @@ public static function requestDataProvider(): \Traversable 'error' => 0, 'tmpName' => '/tmp/php/php1h4j1o', ], - ] + ], ], ]), \array_merge(self::REQUIRED_REQUEST_DATA, [ @@ -243,7 +127,7 @@ public static function requestDataProvider(): \Traversable 'size' => 1235, 'error' => 0, 'tmpName' => '/tmp/php/php2h4j1o', - ] + ], ], 'nested' => [ 'some-key' => [ @@ -253,9 +137,9 @@ public static function requestDataProvider(): \Traversable 'error' => 0, 'tmpName' => '/tmp/php/php1h4j1o', ], - ] + ], ], - ]) + ]), ]; yield [ \array_merge(self::REQUIRED_PAYLOAD_DATA, [ @@ -266,9 +150,9 @@ public static function requestDataProvider(): \Traversable \array_merge(self::REQUIRED_REQUEST_DATA, [ 'attributes' => [ Request::PARSED_BODY_ATTRIBUTE_NAME => false, - 'foo' => 'bar' + 'foo' => 'bar', ], - ]) + ]), ]; } @@ -282,14 +166,14 @@ public static function headersDataProvider(): \Traversable { yield [ ['Content-Type' => ['application/x-www-form-urlencoded']], - ['Content-Type' => new HeaderValue(['value' => ['application/x-www-form-urlencoded']])] + ['Content-Type' => new HeaderValue(['value' => ['application/x-www-form-urlencoded']])], ]; yield [ ['Content-Type' => ['application/x-www-form-urlencoded'], 'X-Test' => ['foo', 'bar']], [ 'Content-Type' => new HeaderValue(['value' => ['application/x-www-form-urlencoded']]), 'X-Test' => new HeaderValue(['value' => ['foo', 'bar']]), - ] + ], ]; yield [['Content-Type' => [null]], []]; yield [['Content-Type' => [1]], []]; @@ -302,15 +186,135 @@ public static function headersDataProvider(): \Traversable [ 'X-Test' => new HeaderValue(['value' => ['foo', 'bar']]), 'X-Test2' => new HeaderValue(['value' => ['foo']]), - ] + ], ]; yield [ ['Content-Type' => 'application/x-www-form-urlencoded'], - ['Content-Type' => new HeaderValue(['value' => ['application/x-www-form-urlencoded']])] + ['Content-Type' => new HeaderValue(['value' => ['application/x-www-form-urlencoded']])], ]; yield [['Content-Type' => new \stdClass()], []]; } + #[DataProvider('requestDataProvider')] + public function testWaitRequestFromArray(array $header, array $expected): void + { + $worker = $this->createMock(WorkerInterface::class); + $worker->expects($this->once()) + ->method('waitPayload') + ->willReturn(new Payload('foo', \json_encode($header))); + + $worker = new HttpWorker($worker); + + $this->assertEquals(new Request(...$expected), $worker->waitRequest()); + } + + #[DataProvider('requestDataProvider')] + public function testWaitRequestFromProto(array $header, array $expected): void + { + $request = self::createProtoRequest($header); + + $worker = $this->createMock(WorkerInterface::class); + $worker->expects($this->once()) + ->method('waitPayload') + ->willReturn(new Payload('foo', $request->serializeToString())); + + $worker = new HttpWorker($worker); + + $this->assertEquals(new Request(...$expected), $worker->waitRequest()); + } + + #[DataProvider('emptyRequestDataProvider')] + public function testWaitRequestWithEmptyData(?Payload $payload): void + { + $worker = $this->createMock(WorkerInterface::class); + $worker->expects($this->once()) + ->method('waitPayload') + ->willReturn($payload); + + $worker = new HttpWorker($worker); + + $this->assertEquals(null, $worker->waitRequest()); + } + + public function testEmptyBodyShouldBeConvertedIntoEmptyArrayWithParsedTrue(): void + { + $request = self::createProtoRequest(\array_merge(self::REQUIRED_REQUEST_DATA, ['parsed' => true])); + + $worker = $this->createMock(WorkerInterface::class); + $worker->expects($this->once()) + ->method('waitPayload') + ->willReturn(new Payload('', $request->serializeToString())); + + $worker = new HttpWorker($worker); + + $request = $worker->waitRequest(); + $this->assertSame([], $request->getParsedBody()); + } + + public function testRespondUnableToSendBodyWithInfoStatusException(): void + { + $worker = new HttpWorker($this->createMock(WorkerInterface::class)); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to send a body with informational status code.'); + $worker->respond(100, 'foo'); + } + + public function testRespondWithProtoCodec(): void + { + $expectedHeader = new Response([ + 'status' => 200, + 'headers' => ['Content-Type' => new HeaderValue(['value' => ['application/x-www-form-urlencoded']])], + ]); + + $worker = $this->createMock(WorkerInterface::class); + $worker->expects($this->once()) + ->method('respond') + ->with(new Payload('foo', $expectedHeader->serializeToString()), Frame::CODEC_PROTO); + + (new \ReflectionProperty(HttpWorker::class, 'codec'))->setValue(Frame::CODEC_PROTO); + $worker = new HttpWorker($worker); + + $worker->respond(200, 'foo', ['Content-Type' => ['application/x-www-form-urlencoded']]); + } + + #[DataProvider('headersDataProvider')] + public function testRespondWithProtoCodecWithHeaders(array $headers, array $expected): void + { + $expectedHeader = new Response(['status' => 200, 'headers' => $expected]); + + $worker = $this->createMock(WorkerInterface::class); + $worker->expects($this->once()) + ->method('respond') + ->with(new Payload('foo', $expectedHeader->serializeToString()), Frame::CODEC_PROTO); + + (new \ReflectionProperty(HttpWorker::class, 'codec'))->setValue(Frame::CODEC_PROTO); + $worker = new HttpWorker($worker); + + $worker->respond(200, 'foo', $headers); + } + + public function testRespondWithJsonCodec(): void + { + $worker = $this->createMock(WorkerInterface::class); + $worker->expects($this->once()) + ->method('respond') + ->with(new Payload('foo', \json_encode([ + 'status' => 200, + 'headers' => ['Content-Type' => ['application/x-www-form-urlencoded']], + ])), Frame::CODEC_JSON); + + (new \ReflectionProperty(HttpWorker::class, 'codec'))->setValue(Frame::CODEC_JSON); + $worker = new HttpWorker($worker); + + $worker->respond(200, 'foo', ['Content-Type' => ['application/x-www-form-urlencoded']]); + } + + protected function tearDown(): void + { + (new \ReflectionProperty(HttpWorker::class, 'codec'))->setValue(null); + } + private static function createProtoRequest(array $values): \RoadRunner\HTTP\DTO\V1\Request { $toHeaderValue = static function (string $key, bool $wrap = true) use (&$values): void { @@ -325,7 +329,7 @@ private static function createProtoRequest(array $values): \RoadRunner\HTTP\DTO\ $toHeaderValue('attributes'); $toHeaderValue('cookies'); - return new \RoadRunner\HTTP\DTO\V1\Request([ + return new \RoadRunner\HTTP\DTO\V1\Request([ 'remote_addr' => $values['remoteAddr'], 'protocol' => $values['protocol'], 'method' => $values['method'], @@ -338,9 +342,4 @@ private static function createProtoRequest(array $values): \RoadRunner\HTTP\DTO\ 'attributes' => $values['attributes'] ?? [], ]); } - - protected function tearDown(): void - { - (new \ReflectionProperty(HttpWorker::class, 'codec'))->setValue(null); - } } diff --git a/tests/Unit/PSR7WorkerTest.php b/tests/Unit/PSR7WorkerTest.php index 2cbf17e..70d002e 100644 --- a/tests/Unit/PSR7WorkerTest.php +++ b/tests/Unit/PSR7WorkerTest.php @@ -14,7 +14,6 @@ use Spiral\RoadRunner\Tests\Http\Unit\Stub\TestRelay; use Spiral\RoadRunner\Worker; - #[CoversClass(PSR7Worker::class)] #[CoversClass(GlobalState::class)] #[RunClassInSeparateProcess] @@ -36,7 +35,7 @@ public function testStateServerLeak(): void [ [ 'Content-Type' => ['application/html'], - 'Connection' => ['keep-alive'] + 'Connection' => ['keep-alive'], ], [ 'REQUEST_URI' => 'http://localhost', @@ -49,14 +48,14 @@ public function testStateServerLeak(): void ], [ [ - 'Content-Type' => ['application/json'] + 'Content-Type' => ['application/json'], ], [ 'REQUEST_URI' => 'http://localhost', 'REMOTE_ADDR' => '127.0.0.1', 'REQUEST_METHOD' => 'GET', 'HTTP_USER_AGENT' => '', - 'CONTENT_TYPE' => 'application/json' + 'CONTENT_TYPE' => 'application/json', ], ], ]; @@ -73,8 +72,8 @@ public function testStateServerLeak(): void 'parsed' => false, ]; - $head = (string)\json_encode($body, \JSON_THROW_ON_ERROR); - $frame = new Frame($head .'test', [\strlen($head)]); + $head = (string) \json_encode($body, \JSON_THROW_ON_ERROR); + $frame = new Frame($head . 'test', [\strlen($head)]); $relay->addFrames($frame); @@ -87,7 +86,6 @@ public function testStateServerLeak(): void } } - protected function tearDown(): void { // Clean all extra output buffers diff --git a/tests/Unit/StreamResponseTest.php b/tests/Unit/StreamResponseTest.php index f785742..fd73172 100644 --- a/tests/Unit/StreamResponseTest.php +++ b/tests/Unit/StreamResponseTest.php @@ -15,11 +15,6 @@ final class StreamResponseTest extends TestCase private TestRelay $relay; private Worker $worker; - protected function tearDown(): void - { - unset($this->relay, $this->worker); - } - /** * Regular case */ @@ -73,6 +68,11 @@ public function testStopStreamResponse(): void self::assertSame('Hello,', $this->getRelay()->getReceivedBody()); } + protected function tearDown(): void + { + unset($this->relay, $this->worker); + } + private function getRelay(): TestRelay { return $this->relay ??= new TestRelay(); diff --git a/tests/Unit/Stub/TestRelay.php b/tests/Unit/Stub/TestRelay.php index 4b2cd34..8c2013c 100644 --- a/tests/Unit/Stub/TestRelay.php +++ b/tests/Unit/Stub/TestRelay.php @@ -28,11 +28,11 @@ public function addFrame( bool $stream = false, bool $stopStream = false, ): self { - $head = (string)\json_encode([ + $head = (string) \json_encode([ 'status' => $status, 'headers' => $headers, ], \JSON_THROW_ON_ERROR); - $frame = new Frame($head .$body, [\strlen($head)]); + $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); @@ -50,7 +50,7 @@ public function getReceived(): array public function getReceivedBody(): string { - return \implode('', \array_map(static fn (Frame $frame) + return \implode('', \array_map(static fn(Frame $frame) => \substr($frame->payload, $frame->options[0]), $this->received)); } From fc8a5ef4683cd720b8f6af80a8b4db6bfae2b023 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 31 Aug 2025 16:36:03 +0400 Subject: [PATCH 3/4] Update psalm --- composer.json | 2 +- psalm.xml | 1 + src/HttpWorker.php | 2 ++ src/PSR7Worker.php | 2 ++ 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f87a829..d4abe97 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ "spiral/code-style": "^2.3", "spiral/dumper": "^3.3", "symfony/process": "^6.2 || ^7.0", - "vimeo/psalm": "^5.9" + "vimeo/psalm": "^6.13" }, "autoload": { "psr-4": { diff --git a/psalm.xml b/psalm.xml index 817a397..d509fb5 100644 --- a/psalm.xml +++ b/psalm.xml @@ -7,6 +7,7 @@ resolveFromConfigFile="true" findUnusedBaselineEntry="false" findUnusedCode="false" + ensureOverrideAttribute="false" > diff --git a/src/HttpWorker.php b/src/HttpWorker.php index d22e63a..dd9528f 100644 --- a/src/HttpWorker.php +++ b/src/HttpWorker.php @@ -35,6 +35,8 @@ * } * * @see Request + * + * @api */ class HttpWorker implements HttpWorkerInterface { diff --git a/src/PSR7Worker.php b/src/PSR7Worker.php index 02e04f4..0f3e230 100644 --- a/src/PSR7Worker.php +++ b/src/PSR7Worker.php @@ -18,6 +18,8 @@ * * @psalm-import-type UploadedFile from Request * @psalm-import-type UploadedFilesList from Request + * + * @api */ class PSR7Worker implements PSR7WorkerInterface { From fe8804ef33b9f6ba6dc84289e2da50ea6120f1db Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 31 Aug 2025 16:39:25 +0400 Subject: [PATCH 4/4] Update metafiles --- .editorconfig | 8 +++++++- .gitattributes | 15 +++++++-------- .gitignore | 29 +++++++++-------------------- composer.json | 2 +- phpunit.xml | 18 ++++++++++++++++-- 5 files changed, 40 insertions(+), 32 deletions(-) diff --git a/.editorconfig b/.editorconfig index a998937..20d4cdf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,8 +8,14 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true -[*.{yml, yaml, sh, conf, neon*}] +[*.yaml] +indent_size = 2 + +[*.yml] indent_size = 2 [*.md] trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes index bc4b7e4..a0650eb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,10 +1,9 @@ * 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 +/.* export-ignore +/tests export-ignore +/phpunit.xml* export-ignore +/psalm.* export-ignore +/psalm-baseline.xml export-ignore +/infection.* export-ignore +/rector.php export-ignore diff --git a/.gitignore b/.gitignore index 4059830..c1e1d81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,9 @@ -# Composer lock file -composer.lock - -# IDEs -/.idea -/.vscode - -# Vendors -/vendor -**/vendor - -# Temp dirs & trash -/tests/server* -clover* -cover* -.DS_Store -*.cache - -.phpunit.cache/ -.phpunit.result.cache +/.* +!/.github/ +!/.php-cs-fixer.dist.php +!/.editorconfig +!/.gitattributes +/runtime/ +/vendor/ +/composer.lock +*.log diff --git a/composer.json b/composer.json index d4abe97..2c5cbee 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "require-dev": { "jetbrains/phpstorm-attributes": "^1.0", "nyholm/psr7": "^1.3", - "phpunit/phpunit": "^10.0", + "phpunit/phpunit": "^10.5", "spiral/code-style": "^2.3", "spiral/dumper": "^3.3", "symfony/process": "^6.2 || ^7.0", diff --git a/phpunit.xml b/phpunit.xml index c12c7b2..43def94 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,13 +1,14 @@ @@ -15,8 +16,21 @@ + + + + + + + + + + src - + + tests + +