Skip to content
51 changes: 51 additions & 0 deletions src/http/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use function explode;
use function filter_var;
use function is_array;
use function is_numeric;
use function is_string;
use function mb_check_encoding;
use function mb_substr;
Expand Down Expand Up @@ -624,6 +625,56 @@ public function getServerParams(): array
return $_SERVER;
}

/**
* Retrieves the server port number for the current request, supporting PSR-7 and Yii2 fallback.
*
* Returns the port number as determined by the PSR-7 adapter if present, checking configured port headers and
* falling back to the 'SERVER_PORT' server parameter if no header is found.
*
* If no adapter is set, this method falls back to the parent implementation.
*
* This enables seamless access to the server port in both PSR-7 and Yii2 environments, supporting interoperability
* with modern HTTP stacks and legacy workflows.
*
* @return int|null Server port number, or `null` if unavailable.
*
* Usage example:
* ```php
* $port = $request->getServerPort();
* ```
*/
public function getServerPort(): int|null
{
if ($this->adapter !== null) {
$headers = $this->getHeaders();

foreach ($this->portHeaders as $portHeader) {
if ($headers->has($portHeader)) {
$headerPort = $headers->get($portHeader);

if (is_string($headerPort)) {
$ports = explode(',', $headerPort);
$firstPort = trim($ports[0]);

if (is_numeric($firstPort)) {
$port = (int) $firstPort;

if ($port >= 1 && $port <= 65535) {
return $port;
}
}
}
}
}

$port = $this->getServerParam('SERVER_PORT');

return is_numeric($port) ? (int) $port : null;
}

return parent::getServerPort();
}

/**
* Retrieves uploaded files from the current request, supporting PSR-7 and Yii2 fallback.
*
Expand Down
107 changes: 107 additions & 0 deletions tests/adapter/ServerParamsPsr7Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use yii\base\InvalidConfigException;
use yii2\extensions\psrbridge\http\Request;
use yii2\extensions\psrbridge\tests\provider\RequestProvider;
use yii2\extensions\psrbridge\tests\provider\ServerParamsPsr7Provider;
use yii2\extensions\psrbridge\tests\support\FactoryHelper;
use yii2\extensions\psrbridge\tests\TestCase;

Expand Down Expand Up @@ -495,4 +496,110 @@ public function testServerNameIndependentRequestsWithDifferentServerNames(): voi
'Independent request instances should return different server names when configured with different values.',
);
}

#[Group('server-port')]
public function testServerPortAfterRequestReset(): void
{
$initialPort = 8080;
$newPort = 9090;

$request = new Request();

$request->setPsr7Request(
FactoryHelper::createRequest('GET', '/test', serverParams: ['SERVER_PORT' => $initialPort]),
);

$result1 = $request->getServerPort();

self::assertSame(
$initialPort,
$result1,
"'SERVER_PORT' should return '{$initialPort}' from initial PSR-7 request.",
);

$request->reset();

$request->setPsr7Request(
FactoryHelper::createRequest('GET', '/test', serverParams: ['SERVER_PORT' => $newPort]),
);

$result2 = $request->getServerPort();

self::assertSame(
$newPort,
$result2,
"'SERVER_PORT' should return '{$newPort}' from new PSR-7 request after 'reset' method.",
);
self::assertNotSame(
$result1,
$result2,
"'SERVER_PORT' should change after request 'reset' method and new PSR-7 request assignment.",
);
}

/**
* @phpstan-param array<string, mixed> $requestConfig
* @phpstan-param array<string, mixed> $serverGlobal
* @phpstan-param array<string, array<int, string>|int|string> $headers
* @phpstan-param array<string, mixed> $serverParams
*/
#[DataProviderExternal(ServerParamsPsr7Provider::class, 'serverPortCases')]
#[Group('server-port')]
public function testServerPortCases(
array $requestConfig,
array $serverGlobal,
array $headers,
array $serverParams,
int|null $expected,
string $message,
): void {
$_SERVER = $serverGlobal;

$request = new Request($requestConfig);

$request->setPsr7Request(
FactoryHelper::createRequest('GET', '/test', $headers, serverParams: $serverParams),
);

self::assertSame($expected, $request->getServerPort(), $message);
}

#[Group('server-port')]
public function testServerPortIndependentRequestsWithDifferentPorts(): void
{
$port1 = 8080;
$port2 = 443;

$request1 = new Request();

$request1->setPsr7Request(
FactoryHelper::createRequest('GET', '/test1', serverParams: ['SERVER_PORT' => $port1]),
);

$request2 = new Request();

$request2->setPsr7Request(
FactoryHelper::createRequest('GET', '/test2', serverParams: ['SERVER_PORT' => $port2]),
);

$result1 = $request1->getServerPort();
$result2 = $request2->getServerPort();

self::assertSame(
$port1,
$result1,
"First request should return '{$port1}' from its PSR-7 'serverParams'.",
);
self::assertSame(
$port2,
$result2,
"Second request should return '{$port2}' from its PSR-7 'serverParams'.",
);
self::assertNotSame(
$result1,
$result2,
"Independent request instances should return different 'SERVER_PORT' when configured with different " .
'values.',
);
}
}
182 changes: 182 additions & 0 deletions tests/provider/ServerParamsPsr7Provider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<?php

declare(strict_types=1);

namespace yii2\extensions\psrbridge\tests\provider;

final class ServerParamsPsr7Provider
{
/**
* @phpstan-return array<
* string,
* array{
* array<string, mixed>,
* array<string, mixed>,
* array<string, array<int, string>|int|string>,
* array<string, mixed>,
* int|null,
* string,
* }
* >
*/
public static function serverPortCases(): array
{
return [
'Forwarded port when request from trusted proxy' => [
[
'portHeaders' => ['X-Forwarded-Port'],
'trustedHosts' => ['10.0.0.0/24'], // trust this subnet
],
['REMOTE_ADDR' => '10.0.0.1'],
['X-Forwarded-Port' => '443'],
[
'SERVER_PORT' => '8080',
'REMOTE_ADDR' => '10.0.0.1',
],
443,
"'getServerPort()' should return forwarded port when request comes from trusted proxy.",
],
'Ignore forwarded port when request from untrusted host' => [
[
'portHeaders' => ['X-Forwarded-Port'],
'secureHeaders' => ['X-Forwarded-Port'],
'trustedHosts' => ['10.0.0.0/24'], // only trust this subnet
],
['REMOTE_ADDR' => '192.168.1.100'],
['X-Forwarded-Port' => '443'],
[
'REMOTE_ADDR' => '192.168.1.100',
'SERVER_PORT' => '8080',
],
8080,
"'getServerPort()' should ignore forwarded port header from untrusted hosts and use 'SERVER_PORT'.",
],
'Null when PSR-7 request server port is empty array' => [
[],
[],
[],
['SERVER_PORT' => []],
null,
"'SERVER_PORT' should return 'null' from PSR-7 'serverParams' when adapter is set but 'SERVER_PORT' " .
'is an empty array.',
],
'Null when PSR-7 request server port is null' => [
[],
[],
[],
['SERVER_PORT' => null],
null,
"'SERVER_PORT' should return 'null' from PSR-7 'serverParams' when adapter is set but 'SERVER_PORT' " .
"is 'null'.",
],
'Null when PSR-7 request server port is not present' => [
[],
[],
[],
['HTTP_HOST' => 'example.com'],
null,
"'SERVER_PORT' should return 'null' from PSR-7 'serverParams' when adapter is set but 'SERVER_PORT' " .
'is not present.',
],
'Null when PSR-7 request server port is not string' => [
[],
[],
[],
['SERVER_PORT' => ['invalid' => 'array']],
null,
"'SERVER_PORT' should return 'null' from PSR-7 'serverParams' when adapter is set but 'SERVER_PORT' " .
'is not a string.',
],
'Server port as integer when PSR-7 server port.' => [
[],
[],
[],
['SERVER_PORT' => '443'],
443,
"'getServerPort()' should return integer value when 'SERVER_PORT' is a numeric string.",
],
'Server port as integer when PSR-7 server port is numeric string' => [
[],
[],
[],
['SERVER_PORT' => '443'],
443,
"'getServerPort()' should return integer value when 'SERVER_PORT' is a numeric string.",
],
'Server port from comma separated forwarded header' => [
[
'portHeaders' => ['X-Forwarded-Port'],
'secureHeaders' => ['X-Forwarded-Port'],
'trustedHosts' => ['127.0.0.1'],
],
['REMOTE_ADDR' => '127.0.0.1'],
['X-Forwarded-Port' => '9443, 7443'],
[
'REMOTE_ADDR' => '127.0.0.1',
'SERVER_PORT' => '80',
],
9443,
"'getServerPort()' should return the first port from a comma-separated 'X-Forwarded-Port' header.",
],
'Server port from first valid forwarded header when multiple configured' => [
[
'portHeaders' => [
'X-Custom-Port',
'X-Forwarded-Port',
'X-Real-Port',
],
'secureHeaders' => [
'X-Custom-Port',
'X-Forwarded-For',
'X-Forwarded-Host',
'X-Forwarded-Port',
'X-Forwarded-Proto',
'X-Real-Port',
],
'trustedHosts' => ['127.0.0.1'],
],
['REMOTE_ADDR' => '127.0.0.1'],
[
'X-Custom-Port' => '',
'X-Forwarded-Port' => '9443',
'X-Real-Port' => '7443',
],
[
'REMOTE_ADDR' => '127.0.0.1',
'SERVER_PORT' => '80',
],
9443,
"'getServerPort()' should return the port from the first valid forwarded header in the configured " .
'list.',
],
'Server port from forwarded header when adapter is set' => [
[
'portHeaders' => ['X-Forwarded-Port'],
'trustedHosts' => ['127.0.0.1'],
],
['REMOTE_ADDR' => '127.0.0.1'],
['X-Forwarded-Port' => '443'],
[
'REMOTE_ADDR' => '127.0.0.1',
'SERVER_PORT' => '8080',
],
443,
"'getServerPort()' should return the port from 'X-Forwarded-Port' header when present, ignoring " .
"'SERVER_PORT' from PSR-7 'serverParams'.",
],
'Server port from PSR-7 request when adapter is set and server port present' => [
[
'portHeaders' => [
'X-Custom-Port',
'X-Forwarded-Port',
],
],
[],
['X-Custom-Port' => ''],
['SERVER_PORT' => '3000'],
3000,
"'getServerPort()' should fallback to 'SERVER_PORT' when all forwarded headers are 'null' or missing.",
],
];
}
}
Loading