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
39 changes: 39 additions & 0 deletions src/adapter/ServerRequestAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
use yii\base\InvalidConfigException;
use yii\helpers\Json;
use yii\web\{Cookie, HeaderCollection};
use yii\web\NotFoundHttpException;
use yii2\extensions\psrbridge\exception\Message;
use yii2\extensions\psrbridge\http\Request;

use function implode;
use function in_array;
Expand Down Expand Up @@ -364,6 +366,43 @@ public function getUrl(): string
return $url;
}

/**
* Resolves the route and parameters from the given {@see Request} instance using Yii2 UrlManager.
*
* Parses the request using Yii2 UrlManager and returns the resolved route and parameters.
*
* If the route is found, combine the parsed parameters with the query parameters from the PSR-7
* ServerRequestAdapter.
*
* @param Request $request Request instance to resolve.
*
* @throws NotFoundHttpException if the route cannot be resolved by UrlManager.
*
* @return array Array containing the resolved route and combined parameters.
*
* @phpstan-return array<array-key, mixed>
*
* Usage example:
* ```php
* [$route, $params] = $adapter->resolve($request);
* ```
*/
public function resolve(Request $request): array
{
/** @phpstan-var array{0: string, 1: array<string, mixed>}|false $result*/
$result = Yii::$app->getUrlManager()->parseRequest($request);

if ($result !== false) {
[$route, $params] = $result;

$combinedParams = $params + $this->psrRequest->getQueryParams();

return [$route, $combinedParams];
}

throw new NotFoundHttpException(Yii::t('yii', Message::PAGE_NOT_FOUND->getMessage()));
}

/**
* Extracts cookies from the PSR-7 ServerRequestInterface without validation.
*
Expand Down
7 changes: 7 additions & 0 deletions src/exception/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ enum Message: string
*/
case NAME_MUST_BE_STRING_OR_NULL = "'name' must be a 'string' or 'null' in file specification.";

/**
* Error when the page is not found.
*
* Format: "Page not found."
*/
case PAGE_NOT_FOUND = 'Page not found.';

/**
* Error when the PSR-7 request adapter is not set.
*
Expand Down
39 changes: 34 additions & 5 deletions src/http/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Psr\Http\Message\{ServerRequestInterface, UploadedFileInterface};
use yii\base\InvalidConfigException;
use yii\web\{CookieCollection, HeaderCollection, UploadedFile};
use yii\web\{CookieCollection, HeaderCollection, NotFoundHttpException, UploadedFile};
use yii2\extensions\psrbridge\adapter\ServerRequestAdapter;
use yii2\extensions\psrbridge\exception\Message;

Expand Down Expand Up @@ -105,7 +105,7 @@ public function getAuthCredentials(): array

/**
* Apache with php-cgi does not pass HTTP Basic authentication to PHP by default.
* To make it work, add one of the following lines to to your .htaccess file:
* To make it work, add one of the following lines to your .htaccess file:
*
* SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
* --OR--
Expand Down Expand Up @@ -313,7 +313,7 @@ public function getParsedBody(): array|object|null
/**
* Retrieves the underlying PSR-7 ServerRequestInterface instance from the adapter.
*
* Returns the PSR-7 {@see ServerRequestInterface} associated with this request via the internal adapter.
* Returns the PSR-7 ServerRequestInterface associated with this request via the internal adapter.
*
* If the adapter is not set, an {@see InvalidConfigException} is thrown to indicate misconfiguration or missing
* bridge setup.
Expand Down Expand Up @@ -519,11 +519,40 @@ public function reset(): void
$this->adapter = null;
}

/**
* Resolves the request parameters using PSR-7 adapter or Yii2 fallback.
*
* Returns an array of resolved request parameters by delegating to the PSR-7 ServerRequestAdapter if present, or
* falling back to the parent Yii2 implementation when no adapter is set.
*
* This method enables seamless interoperability between PSR-7 compatible HTTP stacks and legacy Yii2 workflows,
* ensuring consistent parameter resolution in both environments.
*
* @throws NotFoundHttpException if the route cannot be resolved by UrlManager.
*
* @return array Array of resolved request parameters for the current request.
*
* @phpstan-return array<array-key, mixed>
*
* Usage example:
* ```php
* $params = $request->resolve();
* ```
*/
public function resolve(): array
{
if ($this->adapter !== null) {
return $this->adapter->resolve($this);
}

return parent::resolve();
}

/**
* Sets the PSR-7 ServerRequestInterface instance for the current request.
*
* Assigns a new {@see ServerRequestAdapter} wrapping the provided PSR-7 {@see ServerRequestInterface} to enable
* PSR-7 interoperability for the Yii2 Request component.
* Assigns a new {@see ServerRequestAdapter} wrapping the provided PSR-7 ServerRequestInterface to enable PSR-7
* interoperability for the Yii2 Request component.
*
* This method is used to bridge PSR-7 compatible HTTP stacks with Yii2, allowing request data to be accessed via
* the adapter.
Expand Down
5 changes: 1 addition & 4 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,7 @@ protected function statelessApplication(array $config = []): StatelessApplicatio
'enableStrictParsing' => false,
'enablePrettyUrl' => true,
'rules' => [
[
'pattern' => '/<controller>/<action>/<test:\w+>',
'route' => '<controller>/<action>',
],
'site/update/<id:\d+>' => 'site/update',
],
],
],
Expand Down
138 changes: 135 additions & 3 deletions tests/http/StatelessApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
use Psr\Http\Message\{ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface};
use stdClass;
use Yii;
use yii\base\{InvalidConfigException, Security};
use yii\base\{Exception, InvalidConfigException, Security};
use yii\di\NotInstantiableException;
use yii\helpers\Json;
use yii\i18n\{Formatter, I18N};
use yii\log\Dispatcher;
use yii\web\{AssetManager, Session, UrlManager, User, View};
use yii\web\{AssetManager, NotFoundHttpException, Session, UrlManager, User, View};
use yii2\extensions\psrbridge\exception\Message;
use yii2\extensions\psrbridge\http\{ErrorHandler, Request, Response};
use yii2\extensions\psrbridge\tests\provider\StatelessApplicationProvider;
use yii2\extensions\psrbridge\tests\support\FactoryHelper;
Expand Down Expand Up @@ -694,6 +695,9 @@ public function testMultipleRequestsWithDifferentSessionsInWorkerMode(): void
}
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
public function testRecalculateMemoryLimitAfterResetAndIniChange(): void
{
$originalLimit = ini_get('memory_limit');
Expand Down Expand Up @@ -727,6 +731,9 @@ public function testRecalculateMemoryLimitAfterResetAndIniChange(): void
ini_set('memory_limit', $originalLimit);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
#[RequiresPhpExtension('runkit7')]
public function testRenderExceptionSetsDisplayErrorsInDebugMode(): void
{
Expand Down Expand Up @@ -780,6 +787,9 @@ public function testRenderExceptionSetsDisplayErrorsInDebugMode(): void
@runkit_constant_redefine('YII_ENV_TEST', true);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
#[RequiresPhpExtension('runkit7')]
public function testRenderExceptionWithErrorActionReturningResponseObject(): void
{
Expand Down Expand Up @@ -829,6 +839,9 @@ public function testRenderExceptionWithErrorActionReturningResponseObject(): voi
@runkit_constant_redefine('YII_DEBUG', true);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
public function testRenderExceptionWithRawFormat(): void
{
HTTPFunctions::set_sapi('apache2handler');
Expand Down Expand Up @@ -864,7 +877,7 @@ public function testRenderExceptionWithRawFormat(): void
$body = $response->getBody()->getContents();

self::assertStringContainsString(
'yii\base\Exception',
Exception::class,
$body,
'RAW format response should contain exception class name.',
);
Expand Down Expand Up @@ -1225,6 +1238,47 @@ public function testReturnJsonResponseWithQueryParametersForSiteGetRoute(): void
);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
public function testReturnJsonResponseWithRouteParameterForSiteUpdateRoute(): void
{
$_SERVER = [
'REQUEST_METHOD' => 'GET',
'REQUEST_URI' => 'site/update/123',
];

$request = FactoryHelper::createServerRequestCreator()->createFromGlobals();

$app = $this->statelessApplication();

$response = $app->handle($request);

self::assertSame(
200,
$response->getStatusCode(),
"Response 'status code' should be '200' for 'site/update/123' route in 'StatelessApplication', " .
'indicating a successful update.',
);
self::assertSame(
'application/json; charset=UTF-8',
$response->getHeaderLine('Content-Type'),
"Response 'content-type' should be 'application/json; charset=UTF-8' for 'site/update/123' route in " .
"'StatelessApplication'.",
);
self::assertSame(
'{"site/update":"123"}',
$response->getBody()->getContents(),
"Response 'body' should contain valid JSON with the route parameter for 'site/update/123' in " .
"'StatelessApplication'.",
);
self::assertSame(
'site/update/123',
$request->getUri()->getPath(),
"Request 'path' should be 'site/update/123' for 'site/update/123' route in 'StatelessApplication'.",
);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
Expand Down Expand Up @@ -1846,6 +1900,72 @@ public function testThrowableOccursDuringRequestHandling(): void
);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
public function testThrowNotFoundHttpExceptionWhenStrictParsingDisabledAndRouteIsMissing(): void
{
$_SERVER = [
'REQUEST_METHOD' => 'GET',
'REQUEST_URI' => 'site/profile/123',
];

$request = FactoryHelper::createServerRequestCreator()->createFromGlobals();

$app = $this->statelessApplication();

$response = $app->handle($request);

self::assertSame(
404,
$response->getStatusCode(),
"Response 'status code' should be '404' when accessing a non-existent route in 'StatelessApplication', " .
"indicating a 'Not Found' error.",
);
self::assertSame(
'text/html; charset=UTF-8',
$response->getHeaders()['content-type'][0] ?? '',
"Response 'content-type' should be 'text/html; charset=UTF-8' for 'NotFoundHttpException' in " .
"'StatelessApplication'.",
);
self::assertStringContainsString(
'<pre>Not Found: Page not found.</pre>',
$response->getBody()->getContents(),
"Response 'body' should contain the default not found message '<pre>Not Found: Page not found.</pre>' " .
"when a 'NotFoundHttpException' is thrown in 'StatelessApplication'.",
);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
public function testThrowNotFoundHttpExceptionWhenStrictParsingEnabledAndRouteIsMissing(): void
{
$_SERVER = [
'REQUEST_METHOD' => 'GET',
'REQUEST_URI' => 'site/profile/123',
];

$request = FactoryHelper::createServerRequestCreator()->createFromGlobals();

$app = $this->statelessApplication(
[
'components' => [
'urlManager' => [
'enableStrictParsing' => true,
],
],
],
);

$app->handle($request);

$this->expectException(NotFoundHttpException::class);
$this->expectExceptionMessage(Message::PAGE_NOT_FOUND->getMessage());

$app->request->resolve();
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
Expand All @@ -1870,6 +1990,9 @@ static function () use (&$eventTriggered): void {
self::assertTrue($eventTriggered, "Should trigger '{$eventName}' event during handle()");
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
#[RequiresPhpExtension('runkit7')]
public function testUseErrorViewLogicWithDebugFalseAndException(): void
{
Expand Down Expand Up @@ -1926,6 +2049,9 @@ public function testUseErrorViewLogicWithDebugFalseAndException(): void
@runkit_constant_redefine('YII_DEBUG', true);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
#[RequiresPhpExtension('runkit7')]
public function testUseErrorViewLogicWithDebugFalseAndUserException(): void
{
Expand Down Expand Up @@ -1982,6 +2108,9 @@ public function testUseErrorViewLogicWithDebugFalseAndUserException(): void
@runkit_constant_redefine('YII_DEBUG', true);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
public function testUseErrorViewLogicWithDebugTrueAndUserException(): void
{
$_SERVER = [
Expand Down Expand Up @@ -2033,6 +2162,9 @@ public function testUseErrorViewLogicWithDebugTrueAndUserException(): void
);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
public function testUseErrorViewLogicWithNonHtmlFormat(): void
{
$_SERVER = [
Expand Down
Loading
Loading