Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"jetbrains/phpstorm-attributes": "^1.0",
"nyholm/psr7": "^1.3",
"phpunit/phpunit": "^10.0",
"spiral/dumper": "^3.3",
"symfony/process": "^6.2 || ^7.0",
"vimeo/psalm": "^5.9"
},
Expand Down
60 changes: 60 additions & 0 deletions src/GlobalState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Spiral\RoadRunner\Http;

use function time;
use function microtime;
use function strtoupper;
use function str_replace;
use function implode;

final class GlobalState
{
/**
* @var array<array-key, mixed> Cached state of the $_SERVER superglobal.
*/
private static array $cachedServer = [];

/**
* Cache superglobal $_SERVER to avoid state leaks between requests.
*/
public static function cacheServerVars(): void
{
self::$cachedServer = $_SERVER;
}

/**
* Enrich cached $_SERVER with data from the request.
*
* @return non-empty-array<array-key, mixed> Cached $_SERVER data enriched with request data.
*/
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['REMOTE_ADDR'] = $request->getRemoteAddr();
$server['REQUEST_METHOD'] = $request->method;
$server['HTTP_USER_AGENT'] = '';

foreach ($request->headers as $key => $value) {
$key = strtoupper(str_replace('-', '_', $key));

if ($key == 'CONTENT_TYPE' || $key == 'CONTENT_LENGTH') {
$server[$key] = implode(', ', $value);

continue;
}

$server['HTTP_' . $key] = implode(', ', $value);
}

return $server;
}
}

GlobalState::cacheServerVars();
44 changes: 19 additions & 25 deletions src/PSR7Worker.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ class PSR7Worker implements PSR7WorkerInterface

private readonly HttpWorker $httpWorker;

private readonly array $originalServer;

/**
* @var string[] Valid values for HTTP protocol version
Expand All @@ -46,7 +45,6 @@ public function __construct(
private readonly UploadedFileFactoryInterface $uploadsFactory,
) {
$this->httpWorker = new HttpWorker($worker);
$this->originalServer = $_SERVER;
}

public function getWorker(): WorkerInterface
Expand All @@ -60,18 +58,26 @@ public function getHttpWorker(): HttpWorker
}

/**
* @psalm-suppress DeprecatedMethod
*
* @param bool $populateServer Whether to populate $_SERVER superglobal.
*
* @throws \JsonException
*/
public function waitRequest(): ?ServerRequestInterface
public function waitRequest(bool $populateServer = true): ?ServerRequestInterface
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm interface compatibility for waitRequest signature.

PSR7Worker implements PSR7WorkerInterface; adding a parameter may break signature compatibility if the interface wasn’t updated.

If the interface still declares waitRequest(): ?ServerRequestInterface, either:

  • update the interface to include the optional parameter, or
  • revert the parameter here and expose an alternative opt-out (e.g., a new method or a constructor flag).

🏁 Script executed:

#!/bin/bash
# Verify interface vs implementation signatures
rg -nP -C3 'interface\s+PSR7WorkerInterface\b' --type php
rg -nP -C1 'function\s+waitRequest\s*\(' --type php | sed -n '1,120p'

Length of output: 1158


Align waitRequest signature between PSR7WorkerInterface and PSR7Worker.

PSR7WorkerInterface declares

public function waitRequest(): ?ServerRequestInterface;

but PSR7Worker implements

public function waitRequest(bool $populateServer = true): ?ServerRequestInterface

— update the interface to include the bool $populateServer = true parameter or remove it from the implementation to restore compatibility.

🤖 Prompt for AI Agents
In src/PSR7Worker.php around line 67, the method signature public function
waitRequest(bool $populateServer = true): ?ServerRequestInterface is
incompatible with PSR7WorkerInterface which declares public function
waitRequest(): ?ServerRequestInterface; update the interface to match this
implementation by adding the parameter signature (bool $populateServer = true)
to PSR7WorkerInterface or remove the optional parameter from the PSR7Worker
implementation so both signatures are identical; pick one approach and update
the corresponding declaration, run static analysis/tests to ensure no other
callers break.

{
$httpRequest = $this->httpWorker->waitRequest();
if ($httpRequest === null) {
return null;
}

$_SERVER = $this->configureServer($httpRequest);
$vars = $this->configureServer($httpRequest);

if ($populateServer) {
$_SERVER = $vars;
}

return $this->mapRequest($httpRequest, $_SERVER);
return $this->mapRequest($httpRequest, $vars);
}

/**
Expand All @@ -92,7 +98,7 @@ public function respond(ResponseInterface $response): void

/**
* @return Generator<mixed, scalar|Stringable, mixed, Stringable|scalar|null> Compatible
* with {@see \Spiral\RoadRunner\Http\HttpWorker::respondStream()}.
* with {@see HttpWorker::respondStream}.
*/
private function streamToGenerator(StreamInterface $stream): Generator
{
Expand Down Expand Up @@ -125,32 +131,20 @@ private function streamToGenerator(StreamInterface $stream): Generator
*/
protected function configureServer(Request $request): array
{
$server = $this->originalServer;

$server['REQUEST_URI'] = $request->uri;
$server['REQUEST_TIME'] = $this->timeInt();
$server['REQUEST_TIME_FLOAT'] = $this->timeFloat();
$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));
if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) {
$server[$key] = \implode(', ', $value);
} else {
$server['HTTP_' . $key] = \implode(', ', $value);
}
}

return $server;
return GlobalState::enrichServerVars($request);
}

/**
* @deprecated
*/
protected function timeInt(): int
{
return \time();
}

/**
* @deprecated
*/
protected function timeFloat(): float
{
return \microtime(true);
Expand Down
74 changes: 68 additions & 6 deletions tests/Unit/PSR7WorkerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,89 @@
namespace Spiral\RoadRunner\Tests\Http\Unit;

use Nyholm\Psr7\Factory\Psr17Factory;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\RunClassInSeparateProcess;
use PHPUnit\Framework\TestCase;
use Spiral\RoadRunner\Http\HttpWorker;
use Spiral\Goridge\Frame;
use Spiral\RoadRunner\Http\GlobalState;
use Spiral\RoadRunner\Http\PSR7Worker;
use Spiral\RoadRunner\Tests\Http\Unit\Stub\TestRelay;
use Spiral\RoadRunner\Worker;


#[CoversClass(PSR7Worker::class)]
#[CoversClass(GlobalState::class)]
#[RunClassInSeparateProcess]
final class PSR7WorkerTest extends TestCase
{
public function testHttpWorkerIsAvailable(): void
public function testStateServerLeak(): void
{
$psrFactory = new Psr17Factory();

$psrWorker = new PSR7Worker(
Worker::create(),
$relay = new TestRelay();
$psrWorker = new PSR7Worker(
new Worker($relay),
$psrFactory,
$psrFactory,
$psrFactory,
);

self::assertInstanceOf(HttpWorker::class, $psrWorker->getHttpWorker());
//dataProvider is always random and we need to keep the order
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Clarify the comment about DataProviders order

PHPUnit preserves the order of items provided by a data provider (unless a randomizer plugin is in use). If order is important, it’s fine to keep inline fixtures; otherwise consider a data provider for better test reporting.

Would you like to switch to a data provider? If you want to confirm ordering guarantees, run this web query:


🌐 Web query:

Does PHPUnit guarantee the iteration order of items returned by a data provider?

💡 Result:

PHPUnit does not explicitly guarantee the iteration order of items returned by a data provider. However, in practice, the data provider method returns an array or an iterable, and PHPUnit runs the test with each dataset in the order they are returned from the data provider.

Since the data provider returns a PHP array or iterable, and PHP arrays preserve insertion order (since PHP 5.4), PHPUnit will run the tests in the order the items are provided by the data provider.

So while not explicitly documented as a guarantee by PHPUnit, in typical usage and by PHP's own behavior regarding arrays, the iteration order of data provider items can be considered stable and predictable.

If you rely on a specific order, it's a good practice to:

  • Return the datasets in the desired order from the data provider.
  • Use numerically indexed arrays or ordered structures like generators that yield data in a specific sequence.

If you want to be absolutely certain about order, it is best to check against the PHPUnit documentation or source code to ensure no changes in future versions.

Would you like me to check for any recent documentation or discussions on this topic?


Update misleading comment about DataProvider ordering

The inline comment in tests/Unit/PSR7WorkerTest.php (line 33) is incorrect: PHPUnit runs data‐provider datasets in the order they’re returned (PHP arrays preserve insertion order). To improve clarity and test reporting, either adjust the comment or extract these cases into a named data provider.

Locations to update:

  • tests/Unit/PSR7WorkerTest.php (line 33)

Suggested diff:

-        //dataProvider is always random and we need to keep the order
+        // PHPUnit executes dataProvider items in the order they’re returned
+        // (PHP arrays preserve insertion order). Consider using a named data provider
+        // for clearer test reporting.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//dataProvider is always random and we need to keep the order
// PHPUnit executes dataProvider items in the order they’re returned
// (PHP arrays preserve insertion order). Consider using a named data provider
// for clearer test reporting.
🤖 Prompt for AI Agents
In tests/Unit/PSR7WorkerTest.php at line 33, the comment incorrectly states that
the dataProvider is always random, but PHPUnit actually runs data-provider
datasets in the order they are returned. Update the comment to accurately
reflect that the order is preserved or refactor the test to use a named data
provider method for clarity and better test reporting.

$fixtures = [
[
[
'Content-Type' => ['application/html'],
'Connection' => ['keep-alive']
],
[
'REQUEST_URI' => 'http://localhost',
'REMOTE_ADDR' => '127.0.0.1',
'REQUEST_METHOD' => 'GET',
'HTTP_USER_AGENT' => '',
'CONTENT_TYPE' => 'application/html',
'HTTP_CONNECTION' => 'keep-alive',
],
],
[
[
'Content-Type' => ['application/json']
],
[
'REQUEST_URI' => 'http://localhost',
'REMOTE_ADDR' => '127.0.0.1',
'REQUEST_METHOD' => 'GET',
'HTTP_USER_AGENT' => '',
'CONTENT_TYPE' => 'application/json'
],
],
];

$_SERVER = [];
foreach ($fixtures as [$headers, $expectedServer]) {
$body = [
'headers' => $headers,
'rawQuery' => '',
'remoteAddr' => '127.0.0.1',
'protocol' => 'HTTP/1.1',
'method' => 'GET',
'uri' => 'http://localhost',
'parsed' => false,
];

$head = (string)\json_encode($body, \JSON_THROW_ON_ERROR);
$frame = new Frame($head .'test', [\strlen($head)]);

$relay->addFrames($frame);

$psrWorker->waitRequest();

unset($_SERVER['REQUEST_TIME']);
unset($_SERVER['REQUEST_TIME_FLOAT']);

self::assertEquals($expectedServer, $_SERVER);
}
}


protected function tearDown(): void
{
// Clean all extra output buffers
Expand Down
Loading