├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml ├── phpunit.xml.bak ├── ruleset.xml ├── src ├── filters │ ├── MiddlewareActionFilter.php │ └── auth │ │ └── MiddlewareAuth.php └── web │ ├── Application.php │ ├── ErrorHandler.php │ ├── Request.php │ ├── Response.php │ ├── monitor │ ├── AbstractMonitor.php │ ├── ConnectionMonitor.php │ └── EventMonitor.php │ └── traits │ └── Psr7ResponseTrait.php └── tests ├── .gitignore ├── .rr.yaml ├── AbstractTestCase.php ├── ApplicationTest.php ├── bootstrap.php ├── config └── config.php ├── controllers └── SiteController.php └── rr-worker.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | tests/runtime/* 3 | rr -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019-Present Charles R. Portwood II 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yii2 PSR-7 Bridge 2 | 3 | A PSR-7 bridge and PSR-15 adapter for Yii2 web applications. 4 | 5 | The usecase for this bridge is to enable Yii2 to be utilized with PSR-7 and PSR-15 middlewars and task runners such as RoadRunner and PHP-PM, with _minimal_ code changes to your application (eg requiring no changes to any calls to `Yii::$app->request` and `Yii::$app->response` within your application). 6 | 7 | > Note that this is currently very alpha quality. It "works" in that a PSR7 request is accepted as input and it returns a valid PSR7 response that is _mostly_ in line with what you would expect. 8 | 9 | > However, features and functionality are missing. Many things don't work, others have unexpected side-effects. You are advised _not_ to use this package at this time. 10 | 11 | > See the `Current Status` checklist at the bottom of the README file for what is current implemented and what we could use help with. 12 | 13 | ## What to help out? 14 | 15 | Contributors are welcome! Check out the `Current Status` checklist for things that still need to be implemented, help add tests, or add new features! 16 | 17 | ## Installation 18 | 19 | This package can be installed via Composer: 20 | 21 | ``` 22 | composer require charlesportwoodii/yii2-psr7-bridge 23 | ``` 24 | 25 | ## Tests 26 | 27 | Tests can be run with `phpunit`. 28 | 29 | ```bash 30 | ./vendor/bin/phpunit 31 | ``` 32 | 33 | ## Usage 34 | 35 | Due to the nature of this package, several changes are needed to your application. 36 | 37 | ### Dispatcher 38 | 39 | 1. Modify your `request` and `response` components within your web application config to be instance of `yii\Psr7\web\Request` and `yii\Psr7\web\Response`, respectively: 40 | 41 | ```php 42 | return [ 43 | // Other flags 44 | 'components' => [ 45 | 'request' => [ 46 | 'class' => \yii\Psr7\web\Request::class, 47 | ], 48 | 'response' => [ 49 | 'class' => \yii\Psr7\web\Response::class 50 | ], 51 | // Other components 52 | ] 53 | ]; 54 | ``` 55 | 56 | > If you're using a custom `Request` class, simply have it overload `yii\Psr7\web\Request` to inherit the base functionality. 57 | 58 | 2. Set the following environment variables to your task runner. With RoadRunner, your configuration might look as follows: 59 | 60 | ```yaml 61 | env: 62 | YII_ALIAS_WEBROOT: /path/to/webroot 63 | YII_ALIAS_WEB: '127.0.0.1:8080' 64 | ``` 65 | 66 | > All environment variables _must_ be defined. 67 | 68 | 3. Run your application with a PSR-15 compatible dispatcher. 69 | 70 | For example, to Go/Roadrunner, you can use the component as follows: 71 | 72 | ```php 73 | #!/usr/bin/env php 74 | waitRequest()) { 103 | if (($request instanceof Psr\Http\Message\ServerRequestInterface)) { 104 | try { 105 | $response = $application->handle($request); 106 | $psr7->respond($response); 107 | } catch (\Throwable $e) { 108 | $psr7->getWorker()->error((string)$e); 109 | } 110 | 111 | if ($application->clean()) { 112 | $psr7->getWorker()->stop(); 113 | return; 114 | } 115 | } 116 | } 117 | } catch (\Throwable $e) { 118 | $psr7->getWorker()->error((string)$e); 119 | } 120 | ``` 121 | 122 | ### Worker Crash Protection 123 | 124 | With each request PHP's memory usage will gradually increase. This is unavoidable due to Yii2 not being designed normal PHP call stacks (Nginx + PHP-FPM, Apache2 CGI, etc...), rather than request-request tools such as a RoadRunner. Calling `$application->clean()` will tell you if the current script usage is within 10% of your `memory_limit` ini set. While most workers will handle a out-of-memory crash exception, you can use this method to explicitly tell the current worker to stop and be reconstructed to avoid HTTP 500's thrown by out-of-memory issues. This will also resolve any unexpected memory leaks that occur due to framework memory reservation. 125 | 126 | ### Session 127 | 128 | This library is fully compatible with `yii\web\Session` and classes that descend from it with a few caveats. 129 | 130 | 1. The application component adds the following session ini settings at runtime. Do not overwrite these settings as they are necessary for `yii\web\Session`. 131 | ```php 132 | ini_set('use_cookies', 'false'); 133 | ini_set('use_only_cookies', 'true'); 134 | ``` 135 | 2. Don't access `$application->getSession()` within your worker. 136 | 137 | ### Request 138 | 139 | `yii\Psr7\web\Request` defines a stand-in replacement for `yii\web\Request`. To access the raw PSR-7 object, call `Yii::$app->request->getPsr7Request()`. 140 | 141 | ### Response 142 | 143 | `yii\Psr7\web\Response` directly extends `yii\web\Response`. All functionality is implemented by `yii\Psr7\web\traits\Psr7ResponseTrait`, which you can `use` as a trait within any custom response classes. Alternatively you can directly extend `yii\Psr7\web\Response`. 144 | 145 | ### ErrorHandler 146 | 147 | `yii\Psr7\web\ErrorHandler` implements custom error handling that is compatible with `yii\web\ErrorHandler`. `yii\Psr7\web\Application` automatically utilizes this error handler. If you have a custom error handler have it extend `yii\Psr7\web\ErrorHandler`. 148 | 149 | Normal functionality via `errorAction` is supported. Yii2's standard error and exception pages should work out of the box. 150 | 151 | ### PSR-7 and PSR-15 compatability 152 | 153 | `\yii\Psr7\web\Application` extends `\yii\web\Application` and implements PSR-15's `\Psr\Http\Server\RequestHandlerInterface` providing full PSR-15 compatability. 154 | 155 | #### PSR-7 156 | 157 | If your application doesn't require PSR-15 middlewares, you can simply return a PSR-7 response from the application as follows: 158 | 159 | ```php 160 | $response = $application->handle($request); 161 | $psr7->respond($response); 162 | ``` 163 | 164 | No dispatcher is necessary in this configuration. 165 | 166 | #### PSR-15 with middlewares/utils package 167 | 168 | Since `\yii\Psr7\web\Application` is PSR-15 middleware compatible, you can also use it with any PSR-15 dispatcher. 169 | 170 | > This library does not implement it's own dispatcher, allowing the developer the freedom to use a PSR-15 compatible dispatcher of their choice for middleware handling. 171 | 172 | As an example with `middlewares/utils`: 173 | 174 | ```php 175 | $response = \Middlewares\Utils\Dispatcher::run([ 176 | // new Middleware, 177 | // new NextMiddleware, // and so forth... 178 | function($request, $next) use ($application) { 179 | return $application->handle($request); 180 | } 181 | ], $request); 182 | 183 | // rr response 184 | $psr7->respond($response); 185 | ``` 186 | 187 | ### PSR-15 Middleware Filters 188 | 189 | This package also provides the capabilities to process PSR-15 compatible middlewares on a per route basis via `yii\base\ActionFilter` extension. 190 | 191 | > Middlewares run on a per route basis via these methods aren't 100% PSR-15 compliant as they are executed in their own sandbox independent of the middlewares declared in any dispatcher. These middlewares operately solely within the context of the action filter itself. Middlewares such as `middlewares/request-time` will only meaure the time it takes to run the action filter rather the entire request. If you need these middlewares to function at a higher level chain them in your primary dispatcher, or consider using a native Yii2 ActionFilter. 192 | 193 | #### Authentication 194 | 195 | If your application requires a PSR-15 authentication middleware _not_ provided by an existing `yii\filters\auth\AuthInterface` class, you can use `yii\Psr7\filters\auth\MiddlewareAuth` to process your PSR-15 authentication middleware. 196 | 197 | `\yii\Psr7\filters\auth\MiddlewareAuth` will run the authentication middleware. If a response is returned by your middleware it will send that response. Otherwise, it will look for the `attribute` specified by your authentication middleware, and run `yii\web\User::loginByAccessToken()` with the value stored in that attribute. 198 | 199 | > Note your `yii\web\User` and `IdentityInterface` should be configured to handle the request attribute you provide it. As most authentication middlewares export a attribute with the user information, this should be used to interface back to Yii2's `IdentityInterface`. 200 | 201 | A simple example with `middlewares/http-authentication` is shown as follows using the `username` attribute populated by `Middlewares\BasicAuthentication`. 202 | 203 | ```php 204 | public function behaviors() 205 | { 206 | return \array_merge(parent::behaviors(), [ 207 | [ 208 | 'class' => \yii\Psr7\filters\auth\MiddlewareAuth::class, 209 | 'attribute' => 'username', 210 | 'middleware' => (new \Middlewares\BasicAuthentication( 211 | Yii::$app->user->getUsers() // Assumes your `IdentityInterface` class has a method call `getUsers()` that returns an array of username/password pairs 212 | /** 213 | * Alternatively, just a simple array for you to map back to Yii2 214 | * 215 | * [ 216 | * 'username1' => 'password1', 217 | * 'username2' => 'password2' 218 | * ] 219 | */ 220 | ))->attribute('username') 221 | ] 222 | ]); 223 | } 224 | ``` 225 | 226 | > Note: This class should be compatible with `yii\filters\auth\CompositeAuth` as it returns `null`. Note however that the `yii\web\Response` object _will_ be populated should an HTTP status code or message be returned. If you require custom responses you should extend this class or manually trigger `handleFailure()`. 227 | 228 | #### Other Middlewares 229 | 230 | `yii\Psr7\filters\MiddlewareActionFilter` can be used to process other PSR-15 compatible Middlewares. Each middleware listed will be executed sequentially, and the effective response of that middleware will be returned. 231 | 232 | As an example: `middlewares/client-ip` and `middlewares/uuid` is used to return the response time of the request and a UUID. 233 | 234 | ```php 235 | public function behaviors() 236 | { 237 | return \array_merge(parent::behaviors(), [ 238 | [ 239 | 'class' => \yii\Psr7\filters\MiddlewareActionFilter::class, 240 | 'middlewares' => [ 241 | // Yii::$app->request->getAttribute('client-ip') will return the client IP 242 | new \Middlewares\ClientIp, 243 | // Yii::$app->response->headers['X-Uuid'] will be set 244 | new \Middlewares\Uuid, 245 | ] 246 | ] 247 | ]); 248 | } 249 | ``` 250 | 251 | The middleware handle also supports PSR-15 compatible closures. 252 | 253 | ```php 254 | public function behaviors() 255 | { 256 | return \array_merge(parent::behaviors(), [ 257 | [ 258 | 'class' => \yii\Psr7\filters\MiddlewareActionFilter::class, 259 | 'middlewares' => [ 260 | function ($request, $next) { 261 | // Yii::$app->request->getAttribute('foo') will be set to `bar` 262 | // Yii::$app->response->headers['hello'] will be set to `world` 263 | return $next->handle( 264 | $request->withAttribute('foo', 'bar') 265 | )->withHeader('hello', 'world') 266 | } 267 | ] 268 | ] 269 | ]); 270 | } 271 | ``` 272 | 273 | Middlewares are processed sequentially either until a response is returned (such as an HTTP redirect) or all middlewares have been processed. 274 | 275 | If a response is returned by any middleware executed, the before action filter will return false, and the resulting response will be sent to the client. 276 | 277 | Any request attribute or header added by any previous middleware will be include in the response. 278 | 279 | ## Why does this package exist? 280 | 281 | #### Performance 282 | 283 | The performance benefits of task runners such as RoadRunner and PHP-PM are extremely difficult to ignore. 284 | 285 | While PHP has had incrimental speed improvements from 7.0 (phpng), the performance of web based PHP applications is limited by the need to rebootstrap _every single file_ with each HTTP request. While Nginx + PHP-FPM is fast, even with opcache every file has to read back into memory on each HTTP request. 286 | 287 | PSR-7 servers enable us to keep almost all of our classes and code in memory between requests, which mostly eliminates the biggest performance bottleneck. 288 | 289 | Be sure to check out the [Performance Comparisons](https://github.com/charlesportwoodii/yii2-psr7-bridge/wiki/Performance-Comparisons) wiki page for more information on the actual performance impact on the yii2-app-basic app. 290 | 291 | It is expected that [PHP 7.4 preloading](https://wiki.php.net/rfc/preload) would improve performance further. 292 | 293 | #### PSR-7 and PSR-15 Compatability 294 | 295 | While not strictly the goal of this project, it's becomming more and more difficult to ignore PSR-7 and PSR-15 middlewares. As the Yii2 team has punted PSR-7 compatability to Yii 2.1 or Yii 3, existing Yii2 projects cannot take advantage of a standardized request/response pattern or chained middlewares. 296 | 297 | Developers conforming to PSR-7 and PSR-15 consequently need to re-implement custom middlewares for Yii2, which runs contrary to the `fast, secure, effecient` mantra of Yii2. This library helps to alleviate some of that pain. 298 | 299 | ## How this works 300 | 301 | This package provides three classes within the `yii\Psr7\web` namespace, `Application`, `Request`, and `Response`, and a `Psr7ResponseTrait` trait that can be used to add PSR7 response capabilities to your existing classes that extend `yii\web\Response`. 302 | 303 | To handle inbound requests, the `yii\Psr7\web\Application` component acts as a stand in replacement for `yii\web\Application` for use in your task runner. It's constructor takes the standard Yii2 configuration array, and a additional `ServerRequestInterface` instance. The `Application` component then instantiates a `yii\Psr7\web\Request` object using the `ServerRequestInterface` provided. 304 | 305 | > Since `yii\web\Application::bootstrap` uses the `request` component, the request component needs to be properly constructed during the application constructor, as opposed to simply calling `$app->handleRequest($psr7Request);` 306 | 307 | `yii\Psr7\web\Request` is a stand-in replacement for `yii\web\Request`. It's only purpose is to provide a interface between `ServerRequestInterface` and the standard `yii\web\Request` API. 308 | 309 | Within your modules, controllers, actions, `Yii::$app->request` and `Yii::$app->response` may be used normally without any changes. 310 | 311 | Before the application exists, it will call `getPsr7Response` on your `response` component. If you're using `yii\web\Response`, simply change your `response` component class in your application configuration to `yii\Psr7\web\Response`. If you're using a custom `Response` object, simply add the `yii\Psr7\web\traits\Psr7ResponseTrait` trait to your `Response` object that extends `yii\web\Response` to gain the necessary behaviors. 312 | 313 | ## Limitations 314 | 315 | - File streams currently don't work (eg `yii\web\Response::sendFile`, `yii\web\Response::sendContentAsFile`, `yii\web\Response::sendStreamAsFile`) 316 | - The Yii2 debug toolbar `yii2-debug` will show the wrong request time and memory usage. 317 | - Yii2 can't send `SameSite` cookies 318 | 319 | ## Current Status 320 | 321 | - [x] Implement custom `Application component. 322 | - [x] Convert a PSR7 Request into `yii\web\Request` object. 323 | - [x] Return a simple response. 324 | - [x] Routing. 325 | - [x] Handle `yii\web\Response::$format`. 326 | - [x] Work with standard Yii2 formatters. 327 | - [x] Handle `HeaderCollection`. 328 | - [x] Handle `CookieCollection`. 329 | - [x] Handle `yii\web\Response::$stream` and `yii\web\Response::$content`. 330 | - [x] Implement `yii\web\Response::redirect`. 331 | - [x] Implement `yii\web\Response::refresh`. 332 | - [x] GET query parameters `yii\web\Request::get()`. 333 | - [x] POST parameters `yii\web\Request::post()`. 334 | - [x] `yii\web\Request::getAuthCredentials()`. 335 | - [x] `yii\web\Request::loadCookies()`. 336 | - [x] Per-action Middleware authentication handling. 337 | - [x] Per-action middleware chains. 338 | - [x] Reuse `Application` component instead of re-instantiating in each looåp. 339 | - [x] `yii\web\ErrorHandler` implementation. 340 | - [x] Run `yii-app-basic`. 341 | - [x] Bootstrap with `yii\log\Target`. 342 | - [x] session handling 343 | - [x] `yii-debug`. 344 | - [x] `yii-gii`. 345 | - [x] Fix fatal memory leak under load 346 | - [ ] `yii\filters\auth\CompositeAuth` compatability. 347 | - [x] Implement comparable `sendFile`, `sendStreamAsFile` 348 | - [ ] `yii\web\Request::$methodParam` support. (Not really applicable to `ServerRequestInterface`) 349 | - [ ] Test Coverage 350 | 351 | ----- 352 | 353 | This project is licensed under the BSD-3-Clause license. See [LICENSE](LICENSE) for more details. 354 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "charlesportwoodii/yii2-psr7-bridge", 3 | "description": "A PSR7 Bridge for Yii2", 4 | "type": "library", 5 | "license": "BSD-3-Clause", 6 | "authors": [ 7 | { 8 | "name": "Charles R. Portwood II", 9 | "email": "charlesportwoodii@erianna.com" 10 | } 11 | ], 12 | "minimum-stability": "stable", 13 | "require": { 14 | "yiisoft/yii2": "^2.0.15", 15 | "psr/http-message": "^1.0", 16 | "psr/http-server-handler": "^1.0", 17 | "laminas/laminas-diactoros": "^2.3", 18 | "yidas/yii2-bower-asset": "2.0.13.1" 19 | }, 20 | "require-dev": { 21 | "samdark/yii2-psr-log-target": "^1.0", 22 | "monolog/monolog": "~2.1.0", 23 | "squizlabs/php_codesniffer": "^3.2", 24 | "phpunit/phpunit": "^9.0", 25 | "spiral/roadrunner": "^2.5" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "yii\\Psr7\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "yii\\Psr7\\tests\\": "tests/" 35 | } 36 | }, 37 | "config": { 38 | "allow-plugins": { 39 | "yiisoft/yii2-compose": true, 40 | "yiisoft/yii2-composer": true 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /phpunit.xml.bak: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Yii2 App Basic coding standard 4 | 5 | /vendor/* 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 0 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/filters/MiddlewareActionFilter.php: -------------------------------------------------------------------------------- 1 | request; 47 | } 48 | 49 | /** 50 | * Before action 51 | * 52 | * @param \yii\base\Action $action 53 | * @return void 54 | */ 55 | public function beforeAction($action) 56 | { 57 | // If there are no middlewares attached, skip this behavior 58 | if (!isset($this->middlewares) || empty($this->middlewares)) { 59 | return true; 60 | } 61 | 62 | $instance = $this; 63 | 64 | foreach ($this->middlewares as $middleware) { 65 | $psr7Request = Yii::$app->request->getPsr7Request(); 66 | if ($middleware instanceof \Closure) { 67 | $response = $middleware($psr7Request, $instance); 68 | } else { 69 | $response = $middleware->process($psr7Request, $instance); 70 | } 71 | 72 | // Update the request instance 73 | Yii::$app->request->setPsr7Request( 74 | $instance->getModifiedRequest() 75 | ); 76 | 77 | // Populate the response 78 | Yii::$app->response->withPsr7Response($response); 79 | 80 | // If we got an out-of-spec HTTP code back, kill the status code since we shouldn't send it. 81 | if ($response->getStatusCode() === $this->continueStatusCode) { 82 | Yii::$app->response->setStatusCode(null); 83 | } 84 | 85 | // If we didn't get a continue response, stop processing and return false 86 | if ($response->getStatusCode() !== $this->continueStatusCode) { 87 | return false; 88 | } 89 | } 90 | 91 | return true; 92 | } 93 | 94 | /** 95 | * RequestHandlerInterface mock method to short-circuit PSR-15 middleware processing 96 | * If this method is called, then it indicates that the existing requests have not yet 97 | * returned a response, and that PSR-15 middleware processing has ended for this filter. 98 | * 99 | * An out-of-spec HTTP status code is thrown to not interfere with existing HTTP specifications. 100 | * 101 | * @param ServerRequestInterface $request 102 | * @return ResponseInterface 103 | */ 104 | public function handle(ServerRequestInterface $request): ResponseInterface 105 | { 106 | $this->request = $request; 107 | return new \Laminas\Diactoros\Response\EmptyResponse( 108 | $this->continueStatusCode 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/filters/auth/MiddlewareAuth.php: -------------------------------------------------------------------------------- 1 | request; 62 | } 63 | 64 | /** 65 | * Authenticates a user 66 | * 67 | * @param User $user 68 | * @param request $request 69 | * @param Response $response 70 | * @return IdentityInterface|null 71 | */ 72 | public function authenticate($user, $request, $response) 73 | { 74 | if ($this->attribute === null) { 75 | Yii::error('Token attribute not set.', 'yii\Psr7\filters\auth\MiddlewareAuth'); 76 | $response->setStatusCode(500); 77 | $response->content = 'An unexpected error occurred.'; 78 | $this->handleFailure($response); 79 | } 80 | 81 | // Process the PSR-15 middleware 82 | $instance = $this; 83 | $process = $this->middleware->process(Yii::$app->request->getPsr7Request(), $instance); 84 | 85 | // Update the PSR-7 Request object 86 | Yii::$app->request->setPsr7Request( 87 | $instance->getModifiedRequest() 88 | ); 89 | 90 | // If we get a continue status code and the expected user attribute is set 91 | // attempt to log this user in use yii\web\User::loginByAccessToken 92 | if ($process->getStatusCode() === $this->continueStatusCode 93 | && $process->hasHeader(static::TOKEN_ATTRIBUTE_NAME) 94 | ) { 95 | if ($identity = $user->loginByAccessToken( 96 | $process->getHeader(static::TOKEN_ATTRIBUTE_NAME), 97 | \get_class($this) 98 | ) 99 | ) { 100 | return $identity; 101 | } 102 | } 103 | 104 | // Populate the response object 105 | $response->withPsr7Response($process); 106 | unset($process); 107 | return null; 108 | } 109 | 110 | /** 111 | * RequestHandlerInterface mock method to short-circuit PSR-15 middleware processing 112 | * If this method is called, then it indicates that the existing requests have not yet 113 | * returned a response, and that PSR-15 middleware processing has ended for this filter. 114 | * 115 | * An out-of-spec HTTP status code is thrown to not interfere with existing HTTP specifications. 116 | * 117 | * @param ServerRequestInterface $request 118 | * @return ResponseInterface 119 | */ 120 | public function handle(ServerRequestInterface $request): ResponseInterface 121 | { 122 | return new \Laminas\Diactoros\Response\EmptyResponse( 123 | $this->continueStatusCode, 124 | [ 125 | static::TOKEN_ATTRIBUTE_NAME => $request->getAttribute($this->attribute) 126 | ] 127 | ); 128 | } 129 | 130 | /** 131 | * If the authentication event failed, rethrow as an HttpException to end processing 132 | * 133 | * @param Response $response 134 | * @throws HttpException 135 | * @return void 136 | */ 137 | public function handleFailure($response) 138 | { 139 | throw new HttpException( 140 | $response->getStatusCode(), 141 | $response->content 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/web/Application.php: -------------------------------------------------------------------------------- 1 | config = $config; 54 | 55 | // Set the environment aliases 56 | Yii::setAlias('@webroot', \getenv('YII_ALIAS_WEBROOT')); 57 | Yii::setAlias('@web', \getenv('YII_ALIAS_WEB')); 58 | 59 | // This is necessary to get \yii\web\Session to work properly. 60 | ini_set('use_cookies', 'false'); 61 | ini_set('use_only_cookies', 'true'); 62 | 63 | Yii::$app = $this; 64 | static::setInstance($this); 65 | $this->monitors = $this->monitors(); 66 | } 67 | 68 | /** 69 | * Sets up any monitors we want 70 | * 71 | * @return array 72 | */ 73 | public function monitors() 74 | { 75 | return [ 76 | new ConnectionMonitor, 77 | new EventMonitor 78 | ]; 79 | } 80 | 81 | /** 82 | * Re-registers all components with the original configuration 83 | * 84 | * @return void 85 | */ 86 | protected function reset(ServerRequestInterface $request) 87 | { 88 | // Override YII_BEGIN_TIME if possible for yii2-debug 89 | // and other modules that depend on it 90 | if (\function_exists('uopz_redefine')) { 91 | \uopz_redefine('YII_BEGIN_TIME', microtime(true)); 92 | } 93 | 94 | foreach ($this->monitors as $monitor) { 95 | $monitor->on(); 96 | } 97 | 98 | $config = $this->config; 99 | 100 | $config['components']['request']['psr7Request'] = $request; 101 | 102 | $this->state = self::STATE_BEGIN; 103 | $this->preInit($config); 104 | 105 | // Deregister any existing error handler since `ErrorHandler::register()` allocates memory on each request 106 | if ($this->has('errorHandler') && $this->getErrorHandler() !== null) { 107 | $this->getErrorHandler()->unregister(); 108 | } 109 | 110 | $this->registerErrorHandler($config); 111 | Component::__construct($config); 112 | 113 | // Session data has to be explicitly loaded before any bootstrapping occurs to ensure compatability 114 | // with bootstrapped components (such as yii2-debug). 115 | if (($session = $this->getSession()) !== null) { 116 | // Close the session if it was open. 117 | $session->close(); 118 | 119 | // If a session cookie is defined, load it into Yii::$app->session 120 | if (isset($request->getCookieParams()[$session->getName()])) { 121 | $session->setId($request->getCookieParams()[$session->getName()]); 122 | } 123 | } 124 | 125 | // Open the session before any modules that need it are bootstrapped. 126 | $this->ensureBehaviors(); 127 | $session->open(); 128 | $this->bootstrap(); 129 | 130 | // Once bootstrapping is done we can close the session. 131 | // Accessing it in the future will re-open it. 132 | $session->close(); 133 | } 134 | 135 | /** 136 | * {@inheritdoc} 137 | */ 138 | public function init() 139 | { 140 | $this->state = self::STATE_INIT; 141 | } 142 | 143 | /** 144 | * {@inheritdoc} 145 | */ 146 | protected function bootstrap() 147 | { 148 | // Call the bootstrap method in \yii\base\Application instead of \yii\web\Application 149 | $method = new ReflectionMethod(get_parent_class(get_parent_class($this)), 'bootstrap'); 150 | $method->setAccessible(true); 151 | $method->invoke($this); 152 | } 153 | 154 | /** 155 | * PSR-15 RequestHandlerInterface 156 | * 157 | * @param ServerRequestInterface $request 158 | * @return ResponseInterface 159 | */ 160 | public function handle(ServerRequestInterface $request) : ResponseInterface 161 | { 162 | try { 163 | $this->reset($request); 164 | $this->state = self::STATE_BEFORE_REQUEST; 165 | $this->trigger(self::EVENT_BEFORE_REQUEST); 166 | 167 | $this->state = self::STATE_HANDLING_REQUEST; 168 | 169 | $response = $this->handleRequest($this->getRequest()); 170 | 171 | $this->state = self::STATE_AFTER_REQUEST; 172 | $this->trigger(self::EVENT_AFTER_REQUEST); 173 | 174 | $this->state = self::STATE_END; 175 | return $this->terminate($response->getPsr7Response()); 176 | } catch (\Exception $e) { 177 | return $this->terminate($this->handleError($e)); 178 | } catch (\Throwable $e) { 179 | return $this->terminate($this->handleError($e)); 180 | } 181 | } 182 | 183 | /** 184 | * Terminates the application 185 | * 186 | * This method handles final log flushing and session termination 187 | * 188 | * @param ResponseInterface $response 189 | * @return ResponseInterface 190 | */ 191 | protected function terminate(ResponseInterface $response) : ResponseInterface 192 | { 193 | // Handle any monitors that are attached 194 | foreach ($this->monitors as $monitor) { 195 | $monitor->shutdown(); 196 | } 197 | 198 | // Reset fileuploads 199 | \yii\web\UploadedFile::reset(); 200 | 201 | // Reset the logger 202 | if (($logger = Yii::getLogger()) !== null) { 203 | $logger->flush(true); 204 | } 205 | 206 | // Return the parent response 207 | return $response; 208 | } 209 | 210 | /** 211 | * Handles exceptions and errors thrown by the request handler 212 | * 213 | * @param \Throwable|\Exception $exception 214 | * @return ResponseInterface 215 | */ 216 | private function handleError(\Throwable $exception) : ResponseInterface 217 | { 218 | $response = $this->getErrorHandler()->handleException($exception); 219 | 220 | $this->trigger(self::EVENT_AFTER_REQUEST); 221 | $this->state = self::STATE_END; 222 | 223 | return $response->getPsr7Response(); 224 | } 225 | 226 | /** 227 | * {@inheritdoc} 228 | */ 229 | public function coreComponents() 230 | { 231 | return array_merge( 232 | parent::coreComponents(), 233 | [ 234 | 'request' => ['class' => \yii\Psr7\web\Request::class], 235 | 'response' => ['class' => \yii\Psr7\web\Response::class], 236 | 'session' => ['class' => \yii\web\Session::class], 237 | 'user' => ['class' => \yii\web\User::class], 238 | 'errorHandler' => ['class' => \yii\Psr7\web\ErrorHandler::class], 239 | ] 240 | ); 241 | } 242 | 243 | /** 244 | * Cleanup function to be called at the end of the script execution 245 | * This will automatically run garbage collection, and if the script 246 | * is within 5% of the memory limit will pre-maturely kill the worker 247 | * forcing your task-runner to rebuild it. 248 | * 249 | * This is implemented to avoid requests failing due to memory exhaustion 250 | * 251 | * @return boolean 252 | */ 253 | public function clean() 254 | { 255 | gc_collect_cycles(); 256 | $limit = $this->getMemoryLimit(); 257 | $bound = $limit * .90; 258 | $usage = memory_get_usage(true); 259 | if ($usage >= $bound) { 260 | return true; 261 | } 262 | 263 | return false; 264 | } 265 | 266 | /** 267 | * Retrieves the current memory as integer bytes 268 | * 269 | * @return int 270 | */ 271 | private function getMemoryLimit() : int 272 | { 273 | if (!$this->memoryLimit) { 274 | $limit = ini_get('memory_limit'); 275 | sscanf($limit, '%u%c', $number, $suffix); 276 | if (isset($suffix)) { 277 | $number = $number * pow(1024, strpos(' KMG', strtoupper($suffix))); 278 | } 279 | 280 | $this->memoryLimit = $number; 281 | } 282 | 283 | return (int)$this->memoryLimit; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/web/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 24 | 25 | // disable error capturing to avoid recursive errors while handling exceptions 26 | $this->unregister(); 27 | 28 | // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent 29 | // HTTP exceptions will override this value in renderException() 30 | if (PHP_SAPI !== 'cli') { 31 | http_response_code(500); 32 | } 33 | 34 | try { 35 | $this->logException($exception); 36 | if ($this->discardExistingOutput) { 37 | $this->clearOutput(); 38 | } 39 | $response = $this->renderException($exception); 40 | if (!YII_ENV_TEST) { 41 | \Yii::getLogger()->flush(true); 42 | return $response; 43 | } 44 | } catch (\Exception $e) { 45 | // an other exception could be thrown while displaying the exception 46 | return $this->handleFallbackExceptionMessage($e, $exception); 47 | } catch (\Throwable $e) { 48 | // additional check for \Throwable introduced in PHP 7 49 | return $this->handleFallbackExceptionMessage($e, $exception); 50 | } 51 | 52 | $this->exception = null; 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | protected function handleFallbackExceptionMessage($exception, $previousException) 59 | { 60 | $response = new Response; 61 | $msg = "An Error occurred while handling another error:\n"; 62 | $msg .= (string) $exception; 63 | $msg .= "\nPrevious exception:\n"; 64 | $msg .= (string) $previousException; 65 | $response->data = $msg; 66 | if (YII_DEBUG) { 67 | if (PHP_SAPI === 'cli') { 68 | $response->data = $msg; 69 | } else { 70 | $response->data = '
' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '
'; 71 | } 72 | } else { 73 | $response->data = 'An internal server error occurred.'; 74 | } 75 | $response->data .= "\n\$_SERVER = " . VarDumper::export($_SERVER); 76 | error_log($response->data); 77 | 78 | return $response; 79 | } 80 | 81 | /** 82 | * @inheritdoc 83 | */ 84 | protected function renderException($exception) 85 | { 86 | if (Yii::$app->has('response')) { 87 | $response = Yii::$app->getResponse(); 88 | // reset parameters of response to avoid interference with partially created response data 89 | // in case the error occurred while sending the response. 90 | $response->isSent = false; 91 | $response->stream = null; 92 | $response->data = null; 93 | $response->content = null; 94 | } else { 95 | $response = new Response; 96 | } 97 | 98 | $response->setStatusCodeByException($exception); 99 | 100 | $useErrorView = $response->format === Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException); 101 | 102 | if ($useErrorView && $this->errorAction !== null) { 103 | $result = Yii::$app->runAction($this->errorAction); 104 | if ($result instanceof Response) { 105 | $response = $result; 106 | } else { 107 | $response->data = $result; 108 | } 109 | } elseif ($response->format === Response::FORMAT_HTML) { 110 | if ($this->shouldRenderSimpleHtml()) { 111 | // AJAX request 112 | $response->data = '
' . $this->htmlEncode(static::convertExceptionToString($exception)) . '
'; 113 | } else { 114 | // if there is an error during error rendering it's useful to 115 | // display PHP error in debug mode instead of a blank screen 116 | if (YII_DEBUG) { 117 | ini_set('display_errors', 'true'); 118 | } 119 | $file = $useErrorView ? $this->errorView : $this->exceptionView; 120 | $response->data = $this->renderFile( 121 | $file, 122 | [ 123 | 'exception' => $exception, 124 | ] 125 | ); 126 | } 127 | } elseif ($response->format === Response::FORMAT_RAW) { 128 | $response->data = static::convertExceptionToString($exception); 129 | } else { 130 | $response->data = $this->convertExceptionToArray($exception); 131 | } 132 | 133 | return $response; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/web/Request.php: -------------------------------------------------------------------------------- 1 | psr7Request = $request; 78 | } 79 | 80 | /** 81 | * Returns the PSR7 request 82 | * 83 | * @return ServerRequestInterface|null 84 | */ 85 | public function getPsr7Request() :? ServerRequestInterface 86 | { 87 | return $this->psr7Request; 88 | } 89 | 90 | /** 91 | * @inheritdoc 92 | */ 93 | public function resolve() 94 | { 95 | $result = Yii::$app->getUrlManager()->parseRequest($this); 96 | if ($result !== false) { 97 | list($route, $params) = $result; 98 | if ($this->_queryParams === null) { 99 | $this->_queryParams = $params + $this->getPsr7Request()->getQueryParams(); 100 | } 101 | 102 | return [$route, $this->_queryParams]; 103 | } 104 | 105 | throw new NotFoundHttpException(Yii::t('yii', 'Page not found.')); 106 | } 107 | 108 | /** 109 | * @inheritdoc 110 | */ 111 | public function getHeaders() 112 | { 113 | if ($this->_headers === null) { 114 | $this->_headers = new HeaderCollection; 115 | foreach ($this->getPsr7Request()->getHeaders() as $name => $values) { 116 | foreach ($values as $value) { 117 | $this->_headers->add($name, $value); 118 | } 119 | } 120 | $this->filterHeaders($this->_headers); 121 | } 122 | 123 | return $this->_headers; 124 | } 125 | 126 | /** 127 | * @inheritdoc 128 | */ 129 | public function getMethod() 130 | { 131 | return $this->getPsr7Request()->getMethod(); 132 | } 133 | 134 | /** 135 | * @inheritdoc 136 | */ 137 | public function getRawBody() 138 | { 139 | if ($this->_rawBody === null) { 140 | $request = clone $this->getPsr7Request(); 141 | $body = $request->getBody(); 142 | $body->rewind(); 143 | $this->setRawBody((string)$body->getContents()); 144 | } 145 | 146 | return $this->_rawBody; 147 | } 148 | 149 | /** 150 | * @inheritdoc 151 | */ 152 | public function setRawBody($body) 153 | { 154 | $this->_rawBody = $body; 155 | } 156 | 157 | /** 158 | * @inheritdoc 159 | */ 160 | public function getBodyParams() 161 | { 162 | if ($this->_bodyParams === null) { 163 | $rawContentType = $this->getContentType(); 164 | if (($pos = strpos($rawContentType, ';')) !== false) { 165 | // e.g. text/html; charset=UTF-8 166 | $contentType = substr($rawContentType, 0, $pos); 167 | } else { 168 | $contentType = $rawContentType; 169 | } 170 | 171 | if (isset($this->parsers[$contentType])) { 172 | $parser = Yii::createObject($this->parsers[$contentType]); 173 | if (!($parser instanceof RequestParserInterface)) { 174 | throw new InvalidConfigException("The '$contentType' request parser is invalid. It must implement the yii\\web\\RequestParserInterface."); 175 | } 176 | $this->_bodyParams = $parser->parse($this->getRawBody(), $rawContentType); 177 | } elseif (isset($this->parsers['*'])) { 178 | $parser = Yii::createObject($this->parsers['*']); 179 | if (!($parser instanceof RequestParserInterface)) { 180 | throw new InvalidConfigException('The fallback request parser is invalid. It must implement the yii\\web\\RequestParserInterface.'); 181 | } 182 | $this->_bodyParams = $parser->parse($this->getRawBody(), $rawContentType); 183 | } elseif ($this->getMethod() === 'POST') { 184 | // PHP has already parsed the body so we have all params in $_POST 185 | $this->_bodyParams = $this->getPsr7Request()->getParsedBody(); 186 | } else { 187 | $this->_bodyParams = []; 188 | mb_parse_str($this->getRawBody(), $this->_bodyParams); 189 | } 190 | } 191 | 192 | return $this->_bodyParams; 193 | } 194 | 195 | /** 196 | * @inheritdoc 197 | */ 198 | public function getContentType() 199 | { 200 | return $this->getHeaders()->get('Content-Type') ?? ''; 201 | } 202 | 203 | /** 204 | * @inheritdoc 205 | */ 206 | public function getQueryParams() 207 | { 208 | return $this->_queryParams; 209 | } 210 | 211 | /** 212 | * @inheritdoc 213 | */ 214 | public function getScriptUrl() 215 | { 216 | // The script URL doesn't matter in proxy situations since it is always the bootstrap endpoint. 217 | return ''; 218 | } 219 | 220 | /** 221 | * @inheritdoc 222 | */ 223 | public function getBaseUrl() 224 | { 225 | return Yii::getAlias('@web'); 226 | } 227 | 228 | /** 229 | * @inheritdoc 230 | */ 231 | public function getScriptFile() 232 | { 233 | if (isset($this->_scriptFile)) { 234 | return $this->_scriptFile; 235 | } 236 | 237 | if (isset($this->getServerParams()['SCRIPT_FILENAME'])) { 238 | return $this->getServerParams()['SCRIPT_FILENAME']; 239 | } 240 | 241 | throw new InvalidConfigException('Unable to determine the entry script file path.'); 242 | } 243 | 244 | /** 245 | * @inheritdoc 246 | */ 247 | protected function resolvePathInfo() 248 | { 249 | $pathInfo = $this->getUrl(); 250 | if (($pos = strpos($pathInfo, '?')) !== false) { 251 | $pathInfo = substr($pathInfo, 0, $pos); 252 | } 253 | 254 | $pathInfo = urldecode($pathInfo); 255 | 256 | // try to encode in UTF8 if not so 257 | // http://w3.org/International/questions/qa-forms-utf-8.html 258 | if (!preg_match( 259 | '%^(?: 260 | [\x09\x0A\x0D\x20-\x7E] # ASCII 261 | | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte 262 | | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs 263 | | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte 264 | | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates 265 | | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 266 | | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 267 | | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 268 | )*$%xs', $pathInfo 269 | ) 270 | ) { 271 | $pathInfo = utf8_encode($pathInfo); 272 | } 273 | 274 | if (strncmp($pathInfo, '/', 1) === 0) { 275 | $pathInfo = substr($pathInfo, 1); 276 | } 277 | 278 | return (string)$pathInfo; 279 | } 280 | 281 | /** 282 | * @inheritdoc 283 | */ 284 | protected function resolveRequestUri() 285 | { 286 | $uri = $this->getPsr7Request()->getUri(); 287 | $requestUri = $uri->getPath(); 288 | $queryString = $this->getQueryString(); 289 | if ($queryString !== '') { 290 | $requestUri .= '?' . $this->getQueryString(); 291 | } 292 | 293 | if ($uri->getFragment() !== '') { 294 | $requestUri .= '#' . $uri->getFragment(); 295 | } 296 | 297 | return $requestUri; 298 | } 299 | 300 | /** 301 | * @inheritdoc 302 | */ 303 | public function getQueryString() 304 | { 305 | return $this->getPsr7Request()->getUri()->getQuery(); 306 | } 307 | 308 | /** 309 | * @inheritdoc 310 | */ 311 | public function getServerParams() 312 | { 313 | return $this->getPsr7Request()->getServerParams(); 314 | } 315 | 316 | /** 317 | * @inheritdoc 318 | */ 319 | public function getIsSecureConnection() 320 | { 321 | if ($this->getPsr7Request()->getUri()->getScheme() === 'https') { 322 | return true; 323 | } 324 | 325 | foreach ($this->secureProtocolHeaders as $header => $values) { 326 | if (($headerValue = $this->headers->get($header, null)) !== null) { 327 | foreach ($values as $value) { 328 | if (strcasecmp($headerValue, $value) === 0) { 329 | return true; 330 | } 331 | } 332 | } 333 | } 334 | 335 | return false; 336 | } 337 | 338 | /** 339 | * @inheritdoc 340 | */ 341 | public function getServerName() 342 | { 343 | return $this->getPsr7Request()->getUri()->getHost(); 344 | } 345 | 346 | /** 347 | * @inheritdoc 348 | */ 349 | public function getServerPort() 350 | { 351 | return $this->getPsr7Request()->getUri()->getPort(); 352 | } 353 | 354 | /** 355 | * @inheritdoc 356 | */ 357 | public function getAuthCredentials() 358 | { 359 | // Go net/http transforms the UserInfo URL component to a Authorization: Basic header 360 | // If this header is present, automatically decode it and treat it as the UserInfo component 361 | $headers = $this->getHeaders(); 362 | if ($headers->has('authorization')) { 363 | $authHeader = $headers->get('authorization'); 364 | if (\substr($authHeader, 0, 6) === 'Basic ') { 365 | $credentials = \base64_decode(\str_replace('Basic ', '', $authHeader)); 366 | if (\strpos($credentials, ':') === false) { 367 | return [null, null]; 368 | } 369 | return \explode(':', $credentials); 370 | } 371 | } 372 | 373 | return [null, null]; 374 | } 375 | 376 | /** 377 | * @inheritdoc 378 | */ 379 | protected function loadCookies() 380 | { 381 | $cookies = []; 382 | if ($this->enableCookieValidation) { 383 | if ($this->cookieValidationKey == '') { 384 | throw new InvalidConfigException(get_class($this) . '::cookieValidationKey must be configured with a secret key.'); 385 | } 386 | 387 | foreach ($this->getPsr7Request()->getCookieParams() as $name => $value) { 388 | if (!is_string($value)) { 389 | continue; 390 | } 391 | 392 | $value = \urldecode($value); 393 | 394 | $data = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey); 395 | if ($data === false) { 396 | continue; 397 | } 398 | 399 | $data = @unserialize($data); 400 | if (is_array($data) && isset($data[0], $data[1]) && $data[0] === $name) { 401 | $cookies[$name] = Yii::createObject( 402 | [ 403 | 'class' => 'yii\web\Cookie', 404 | 'name' => $name, 405 | 'value' => $data[1], 406 | 'expire' => null, 407 | ] 408 | ); 409 | } 410 | } 411 | } else { 412 | foreach ($this->getPsr7Request()->getCookieParams() as $name => $value) { 413 | $cookies[$name] = Yii::createObject( 414 | [ 415 | 'class' => 'yii\web\Cookie', 416 | 'name' => $name, 417 | 'value' => $value, 418 | 'expire' => null, 419 | ] 420 | ); 421 | } 422 | } 423 | return $cookies; 424 | } 425 | 426 | /** 427 | * Retrieve attributes derived from the request. 428 | * 429 | * The request "attributes" may be used to allow injection of any 430 | * parameters derived from the request: e.g., the results of path 431 | * match operations; the results of decrypting cookies; the results of 432 | * deserializing non-form-encoded message bodies; etc. Attributes 433 | * will be application and request specific, and CAN be mutable. 434 | * 435 | * @return mixed[] Attributes derived from the request. 436 | */ 437 | public function getAttributes() 438 | { 439 | return $this->getPsr7Request()->getAttributes(); 440 | } 441 | 442 | /** 443 | * Retrieve a single derived request attribute. 444 | * 445 | * Retrieves a single derived request attribute as described in 446 | * getAttributes(). If the attribute has not been previously set, returns 447 | * the default value as provided. 448 | * 449 | * This method obviates the need for a hasAttribute() method, as it allows 450 | * specifying a default value to return if the attribute is not found. 451 | * 452 | * @see getAttributes() 453 | * @param string $name The attribute name. 454 | * @param mixed $default Default value to return if the attribute does not exist. 455 | * @return mixed 456 | */ 457 | public function getAttribute($name, $default = null) 458 | { 459 | return $this->getPsr7Request()->getAttribute($name, $default); 460 | } 461 | 462 | /** 463 | * @inheritdoc 464 | */ 465 | public function getHostInfo() 466 | { 467 | if ($this->_hostInfo === null) { 468 | $secure = $this->getIsSecureConnection(); 469 | $http = $secure ? 'https' : 'http'; 470 | 471 | if ($this->headers->has('X-Forwarded-Host')) { 472 | $this->_hostInfo = $http . '://' . $this->headers->get('X-Forwarded-Host'); 473 | } elseif ($this->headers->has('Host')) { 474 | $this->_hostInfo = $http . '://' . $this->headers->get('Host'); 475 | } elseif (isset($this->getServerParams()['SERVER_NAME'])) { 476 | $this->_hostInfo = $http . '://' . $this->getServerParams()['SERVER_NAME']; 477 | $port = $secure ? $this->getSecurePort() : $this->getPort(); 478 | if (($port !== 80 && !$secure) || ($port !== 443 && $secure)) { 479 | $this->_hostInfo .= ':' . $port; 480 | } 481 | } 482 | } 483 | 484 | return $this->_hostInfo; 485 | } 486 | } 487 | 488 | -------------------------------------------------------------------------------- /src/web/Response.php: -------------------------------------------------------------------------------- 1 | stream = new Stream($this->stream[0], 'r'); 18 | return $response; 19 | } 20 | 21 | /** 22 | * {@inheritDoc} 23 | */ 24 | public function sendFile($filePath, $attachmentName = null, $options = []) 25 | { 26 | if ($stream = fopen($filePath, 'r')) { 27 | return $this->sendStreamAsFile($stream, $attachmentName, $options); 28 | } 29 | 30 | throw new \yii\web\ServerErrorHttpException; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/web/monitor/AbstractMonitor.php: -------------------------------------------------------------------------------- 1 | handler = function (Event $e) { 26 | if ($e->sender instanceof Connection) { 27 | $this->connections[] = $e->sender; 28 | } 29 | }; 30 | } 31 | 32 | public function on() : void 33 | { 34 | Event::on(Connection::class, Connection::EVENT_AFTER_OPEN, $this->handler); 35 | } 36 | 37 | public function off() : void 38 | { 39 | Event::off(Connection::class, Connection::EVENT_AFTER_OPEN, $this->handler); 40 | } 41 | 42 | public function shutdown() : void 43 | { 44 | foreach ($this->connections as $connection) { 45 | $connection->close(); 46 | } 47 | 48 | $this->connections = []; 49 | 50 | 51 | $this->off(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/web/monitor/EventMonitor.php: -------------------------------------------------------------------------------- 1 | handler = function (Event $event) { 31 | $class = $event->sender; 32 | $this->events[] = $event; 33 | }; 34 | } 35 | 36 | public function on() : void 37 | { 38 | Event::on('*', '*', $this->handler); 39 | } 40 | 41 | public function off() : void 42 | { 43 | Event::off('*', '*', $this->handler); 44 | } 45 | 46 | public function shutdown() : void 47 | { 48 | foreach (\array_reverse($this->events) as $event) { 49 | $event->sender->off($event->name); 50 | } 51 | 52 | $this->events = []; 53 | $this->off(); 54 | 55 | if (method_exists(Event::class, 'offAll')) { 56 | Event::offAll(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/web/traits/Psr7ResponseTrait.php: -------------------------------------------------------------------------------- 1 | setStatusCode($response->getStatusCode()); 24 | $this->content = (string)$response->getBody(); 25 | foreach ($response->getHeaders() as $name => $value) { 26 | $this->headers->add($name, $value); 27 | } 28 | 29 | return $this; 30 | } 31 | 32 | /** 33 | * Returns a PSR7 response 34 | * 35 | * @return ResponseInterface 36 | */ 37 | public function getPsr7Response(): ResponseInterface 38 | { 39 | $this->trigger(self::EVENT_BEFORE_SEND); 40 | $this->prepare(); 41 | $this->trigger(self::EVENT_AFTER_PREPARE); 42 | $stream = $this->getPsr7Content(); 43 | 44 | // If a session is defined transform it into a `yii\web\Cookie` instance then close the session. 45 | if (($session = Yii::$app->getSession()) !== null) { 46 | $this->cookies->add( 47 | new Cookie( 48 | [ 49 | 'name' => $session->getName(), 50 | 'value' => $session->id, 51 | 'path' => ini_get('session.cookie_path') 52 | ] 53 | ) 54 | ); 55 | $session->close(); 56 | } 57 | 58 | $response = new Response( 59 | $stream, 60 | $this->getStatusCode() 61 | ); 62 | 63 | // Manually set headers to ensure array headers are added. 64 | foreach ($this->getPsr7Headers() as $header => $value) { 65 | if (\is_array($header)) { 66 | foreach ($header as $v) { 67 | $response = $response->withAddedHeader($header, $v); 68 | } 69 | } else { 70 | $response = $response->withHeader($header, $value); 71 | } 72 | } 73 | 74 | 75 | $this->trigger(self::EVENT_AFTER_SEND); 76 | $this->isSent = true; 77 | 78 | return $response; 79 | } 80 | 81 | /** 82 | * Returns all headers to be sent to the client 83 | * 84 | * @return array 85 | */ 86 | private function getPsr7Headers(): array 87 | { 88 | $headers = []; 89 | foreach ($this->getHeaders() as $name => $values) { 90 | $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name))); 91 | // set replace for first occurrence of header but false afterwards to allow multiple 92 | $replace = true; 93 | foreach ($values as $value) { 94 | if ($replace) { 95 | $headers[$name] = $value; 96 | } 97 | $replace = false; 98 | } 99 | } 100 | 101 | return \array_merge($headers, $this->getPsr7Cookies()); 102 | } 103 | 104 | /** 105 | * Convers the PSR-7 header cookies to raw headers 106 | * 107 | * @return array 108 | */ 109 | private function getPsr7Cookies(): array 110 | { 111 | $cookies = []; 112 | $request = Yii::$app->getRequest(); 113 | if ($request->enableCookieValidation) { 114 | if ($request->cookieValidationKey == '') { 115 | throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.'); 116 | } 117 | $validationKey = $request->cookieValidationKey; 118 | } 119 | 120 | foreach ($this->getCookies() as $cookie) { 121 | $value = $cookie->value; 122 | if ($cookie->expire != 1 && isset($validationKey)) { 123 | $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey); 124 | } 125 | 126 | $data = "$cookie->name=" . \urlencode($value); 127 | 128 | if ($cookie->expire) { 129 | $data .= "; Expires={$cookie->expire}"; 130 | } 131 | 132 | if (!empty($cookie->path)) { 133 | $data .= "; Path={$cookie->path}"; 134 | } 135 | 136 | if (!empty($cookie->domain)) { 137 | $data .= "; Domain={$cookie->domain}"; 138 | } 139 | 140 | if ($cookie->secure) { 141 | $data .= "; Secure"; 142 | } 143 | 144 | if ($cookie->httpOnly) { 145 | $data .= "; HttpOnly"; 146 | } 147 | 148 | $cookies['Set-Cookie'][] = $data; 149 | } 150 | 151 | return $cookies; 152 | } 153 | 154 | /** 155 | * Returns the PSR7 Stream 156 | * 157 | * @return stream 158 | */ 159 | private function getPsr7Content() 160 | { 161 | if ($this->stream === null) { 162 | $stream = fopen('php://memory', 'r+'); 163 | fwrite($stream, $this->content ?? ''); 164 | rewind($stream); 165 | $this->stream = $stream; 166 | } 167 | 168 | return $this->stream; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charlesportwoodii/yii2-psr7-bridge/94c3c461a059541c4bc5e489b657c5f96bd62c75/tests/.gitignore -------------------------------------------------------------------------------- /tests/.rr.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | http: 3 | address: "0.0.0.0:8080" 4 | pool.debug: true 5 | middleware: ["static"] 6 | static: 7 | dir: "./" 8 | rpc: 9 | enable: true 10 | server: 11 | command: "php ./rr-worker.php" 12 | env: 13 | YII_ALIAS_WEB: "http://127.0.0.1:8080" 14 | YII_ALIAS_WEBROOT: ./ 15 | -------------------------------------------------------------------------------- /tests/AbstractTestCase.php: -------------------------------------------------------------------------------- 1 | config = include __DIR__ . '/bootstrap.php'; 20 | $this->app = new Application($this->config); 21 | $this->assertInstanceOf('\yii\Psr7\web\Application', $this->app); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/ApplicationTest.php: -------------------------------------------------------------------------------- 1 | response->format with a simple JSON response 12 | */ 13 | public function testIndexWithJsonResponse() 14 | { 15 | $request = ServerRequestFactory::fromGlobals(); 16 | 17 | $response = $this->app->handle($request); 18 | 19 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 20 | $this->assertEquals(200, $response->getStatusCode()); 21 | $this->assertEquals('application/json; charset=UTF-8', $response->getHeaders()['Content-Type'][0]); 22 | 23 | $body = $response->getBody()->getContents(); 24 | $this->assertEquals('{"hello":"world"}', $body); 25 | } 26 | 27 | /** 28 | * Tests Yii::$app->response->setStatusCode() 29 | */ 30 | public function testCustomStatusCode() 31 | { 32 | $request = ServerRequestFactory::fromGlobals([ 33 | 'REQUEST_URI' => 'site/statuscode', 34 | 'REQUEST_METHOD' => 'GET' 35 | ]); 36 | 37 | $response = $this->app->handle($request); 38 | 39 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 40 | $this->assertEquals(201, $response->getStatusCode()); 41 | } 42 | 43 | /** 44 | * Tests HTTP 302 redirects 45 | */ 46 | public function testRedirect() 47 | { 48 | $request = ServerRequestFactory::fromGlobals([ 49 | 'REQUEST_URI' => 'site/redirect', 50 | 'REQUEST_METHOD' => 'GET' 51 | ]); 52 | 53 | $response = $this->app->handle($request); 54 | 55 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 56 | $this->assertEquals(302, $response->getStatusCode()); 57 | $this->assertEquals('/site/index', $response->getHeaders()['Location'][0]); 58 | } 59 | 60 | /** 61 | * Tests Yii::$app->response->refresh() with URI fragment 62 | */ 63 | public function testFragment() 64 | { 65 | $request = ServerRequestFactory::fromGlobals([ 66 | 'REQUEST_URI' => 'site/refresh', 67 | 'REQUEST_METHOD' => 'GET' 68 | ]); 69 | 70 | $response = $this->app->handle($request); 71 | 72 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 73 | $this->assertEquals(302, $response->getStatusCode()); 74 | $this->assertEquals('site/refresh#foo', $response->getHeaders()['Location'][0]); 75 | } 76 | 77 | /** 78 | * Sends GET request with a Query string and verify the JSON response matches 79 | */ 80 | public function testGetWithQueryParams() 81 | { 82 | $request = ServerRequestFactory::fromGlobals( 83 | [ 84 | 'REQUEST_URI' => 'site/get', 85 | 'REQUEST_METHOD' => 'GET' 86 | ], 87 | [ 88 | 'foo' => 'bar', 89 | 'a' => [ 90 | 'b' => 'c' 91 | ] 92 | ] 93 | ); 94 | 95 | $response = $this->app->handle($request); 96 | 97 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 98 | $this->assertEquals(200, $response->getStatusCode()); 99 | 100 | $body = $response->getBody()->getContents(); 101 | $this->assertEquals('{"foo":"bar","a":{"b":"c"}}', $body); 102 | } 103 | 104 | /** 105 | * Sends POST request and verify the JSON response matches 106 | */ 107 | public function testPostWithRequestBody() 108 | { 109 | $request = ServerRequestFactory::fromGlobals( 110 | [ 111 | 'REQUEST_URI' => 'site/post', 112 | 'REQUEST_METHOD' => 'POST' 113 | ], 114 | null, 115 | [ 116 | 'foo' => 'bar', 117 | 'a' => [ 118 | 'b' => 'c' 119 | ] 120 | ] 121 | ); 122 | 123 | $response = $this->app->handle($request); 124 | 125 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 126 | $this->assertEquals(200, $response->getStatusCode()); 127 | 128 | $body = $response->getBody()->getContents(); 129 | $this->assertEquals('{"foo":"bar","a":{"b":"c"}}', $body); 130 | } 131 | 132 | /** 133 | * Tests that cookie headers are set 134 | */ 135 | public function testSetCookie() 136 | { 137 | $request = ServerRequestFactory::fromGlobals([ 138 | 'REQUEST_URI' => 'site/cookie', 139 | 'REQUEST_METHOD' => 'GET' 140 | ]); 141 | 142 | $response = $this->app->handle($request); 143 | 144 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 145 | $this->assertEquals(200, $response->getStatusCode()); 146 | $cookies = $response->getHeaders()['Set-Cookie']; 147 | foreach ($cookies as $i => $cookie) { 148 | // Skip the PHPSESSION header 149 | if ($i + 1 == count($cookies)) { 150 | continue; 151 | } 152 | $params = \explode('; ', $cookie); 153 | $this->assertTrue( 154 | \in_array( 155 | $params[0], 156 | [ 157 | 'test=test', 158 | 'test2=test2' 159 | ] 160 | ) 161 | ); 162 | } 163 | } 164 | 165 | /** 166 | * Verifies that cookies passed to the server are recieved 167 | */ 168 | public function testGetCookie() 169 | { 170 | $request = ServerRequestFactory::fromGlobals( 171 | [ 172 | 'REQUEST_URI' => 'site/getcookies', 173 | 'REQUEST_METHOD' => 'GET' 174 | ], 175 | null, 176 | null, 177 | [ 178 | 'test' => 'test' 179 | ] 180 | ); 181 | 182 | $response = $this->app->handle($request); 183 | 184 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 185 | $this->assertEquals(200, $response->getStatusCode()); 186 | $body = $response->getBody()->getContents(); 187 | $testbody = '{"test":{"name":"test","value":"test","domain":"","expire":null,"path":"/","secure":false,"httpOnly":true,"sameSite":"Lax"}}'; 188 | $this->assertEquals($testbody, $body); 189 | } 190 | 191 | /** 192 | * Tests HTTP Basic Auth headers are recieved 193 | */ 194 | public function testAuth() 195 | { 196 | $request = ServerRequestFactory::fromGlobals([ 197 | 'REQUEST_URI' => 'site/auth', 198 | 'REQUEST_METHOD' => 'GET', 199 | 'HTTP_authorization' => 'Basic ' . \base64_encode('foo:bar') 200 | ]); 201 | 202 | $response = $this->app->handle($request); 203 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 204 | $this->assertEquals(200, $response->getStatusCode()); 205 | $body = $response->getBody()->getContents(); 206 | $this->assertEquals('{"username":"foo","password":"bar"}', $body); 207 | } 208 | 209 | /** 210 | * Tests HTTP Basic Auth headers are recieved 211 | */ 212 | public function testAuthWithBadHeaders() 213 | { 214 | $request = ServerRequestFactory::fromGlobals([ 215 | 'REQUEST_URI' => 'site/auth', 216 | 'REQUEST_METHOD' => 'GET', 217 | 'HTTP_authorization' => 'Basic foo:bar' 218 | ]); 219 | 220 | $response = $this->app->handle($request); 221 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 222 | $this->assertEquals(200, $response->getStatusCode()); 223 | $body = $response->getBody()->getContents(); 224 | $this->assertEquals('{"username":null,"password":null}', $body); 225 | } 226 | 227 | public function test404() 228 | { 229 | $request = ServerRequestFactory::fromGlobals([ 230 | 'REQUEST_URI' => 'site/404', 231 | 'REQUEST_METHOD' => 'GET' 232 | ]); 233 | 234 | $response = $this->app->handle($request); 235 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 236 | $this->assertEquals(404, $response->getStatusCode()); 237 | } 238 | 239 | public function testGeneralException() 240 | { 241 | $request = ServerRequestFactory::fromGlobals([ 242 | 'REQUEST_URI' => 'site/general-exception', 243 | 'REQUEST_METHOD' => 'GET' 244 | ]); 245 | 246 | $response = $this->app->handle($request); 247 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 248 | $this->assertEquals(500, $response->getStatusCode()); 249 | } 250 | 251 | public function testQueryParametersAlignWithYiiWebRequestInstance() 252 | { 253 | $request = ServerRequestFactory::fromGlobals([ 254 | 'REQUEST_URI' => 'site/query/foo?q=1', 255 | 'REQUEST_METHOD' => 'GET' 256 | ], [ 257 | 'q' => 1 258 | ]); 259 | 260 | $response = $this->app->handle($request); 261 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 262 | $this->assertEquals(200, $response->getStatusCode()); 263 | 264 | $body = $response->getBody()->getContents(); 265 | $this->assertEquals('{"test":"foo","q":1,"queryParams":{"test":"foo","q":1}}', $body); 266 | } 267 | 268 | public function testFileStream() 269 | { 270 | $request = ServerRequestFactory::fromGlobals([ 271 | 'REQUEST_URI' => 'site/stream', 272 | 'REQUEST_METHOD' => 'GET' 273 | ]); 274 | 275 | $response = $this->app->handle($request); 276 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 277 | $this->assertEquals(200, $response->getStatusCode()); 278 | 279 | $body = $response->getBody()->getContents(); 280 | $this->assertEquals('text/yaml', $response->getHeaders()['Content-Type'][0]); 281 | } 282 | 283 | public function testFile() 284 | { 285 | $request = ServerRequestFactory::fromGlobals([ 286 | 'REQUEST_URI' => 'site/file', 287 | 'REQUEST_METHOD' => 'GET' 288 | ]); 289 | 290 | $response = $this->app->handle($request); 291 | $this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $response); 292 | $this->assertEquals(200, $response->getStatusCode()); 293 | 294 | $body = $response->getBody()->getContents(); 295 | $this->assertEquals('text/yaml', $response->getHeaders()['Content-Type'][0]); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | pushHandler(new StreamHandler(__DIR__ . '/../runtime/test.log')); 9 | 10 | return [ 11 | 'id' => 'yii2-psr7-bridge-test-app', 12 | 'basePath' => dirname(__DIR__), 13 | 'bootstrap' => ['log'], 14 | 'controllerNamespace' => '\yii\Psr7\tests\controllers', 15 | 'components' => [ 16 | 'request' => [ 17 | 'class' => \yii\Psr7\web\Request::class, 18 | 'enableCookieValidation' => false, 19 | 'enableCsrfValidation' => false, 20 | 'enableCsrfCookie' => false, 21 | 'parsers' => [ 22 | 'application/json' => \yii\web\JsonParser::class, 23 | ] 24 | ], 25 | 'cache' => [ 26 | 'class' => \yii\caching\FileCache::class 27 | ], 28 | 'response' => [ 29 | 'class' => \yii\Psr7\web\Response::class, 30 | 'charset' => 'UTF-8' 31 | ], 32 | 'user' => [ 33 | 'enableAutoLogin' => false, 34 | ], 35 | 'log' => [ 36 | 'traceLevel' => YII_DEBUG ? 3 : 0, 37 | 'targets' => [ 38 | [ 39 | 'class' => \samdark\log\PsrTarget::class, 40 | 'logger' => $logger, 41 | 'levels' => ['info', 'warning', 'error'], 42 | 'logVars' => [] 43 | ], 44 | ], 45 | ], 46 | 'urlManager' => [ 47 | 'class' => \yii\web\UrlManager::class, 48 | 'showScriptName' => false, 49 | 'enableStrictParsing' => false, 50 | 'enablePrettyUrl' => true, 51 | 'rules' => [ 52 | [ 53 | 'pattern' => '///', 54 | 'route' => '/' 55 | ], 56 | ] 57 | ] 58 | ], 59 | ]; 60 | -------------------------------------------------------------------------------- /tests/controllers/SiteController.php: -------------------------------------------------------------------------------- 1 | response; 17 | $response->format = Response::FORMAT_JSON; 18 | return [ 19 | 'hello' => 'world', 20 | ]; 21 | } 22 | 23 | public function actionStatuscode() 24 | { 25 | Yii::$app->response->statusCode = 201; 26 | } 27 | 28 | public function actionRedirect() 29 | { 30 | $response = Yii::$app->response->redirect('/site/index'); 31 | return; 32 | } 33 | 34 | public function actionRefresh() 35 | { 36 | $response = Yii::$app->response->refresh('#foo'); 37 | return; 38 | } 39 | 40 | public function actionPost() 41 | { 42 | $response = Yii::$app->response; 43 | $response->format = Response::FORMAT_JSON; 44 | return Yii::$app->request->post(); 45 | } 46 | 47 | public function actionGet() 48 | { 49 | $response = Yii::$app->response; 50 | $response->format = Response::FORMAT_JSON; 51 | return Yii::$app->request->get(); 52 | } 53 | 54 | public function actionCookie() 55 | { 56 | $response = Yii::$app->response; 57 | $response->cookies->add( 58 | new Cookie( 59 | [ 60 | 'name' => 'test', 61 | 'value' => 'test', 62 | 'httpOnly' => false 63 | ] 64 | ) 65 | ); 66 | 67 | $response->cookies->add( 68 | new Cookie( 69 | [ 70 | 'name' => 'test2', 71 | 'value' => 'test2' 72 | ] 73 | ) 74 | ); 75 | } 76 | 77 | public function actionGetcookies() 78 | { 79 | $response = Yii::$app->response; 80 | $response->format = Response::FORMAT_JSON; 81 | return Yii::$app->request->getCookies(); 82 | } 83 | 84 | public function actionAuth() 85 | { 86 | $response = Yii::$app->response; 87 | $response->format = Response::FORMAT_JSON; 88 | return [ 89 | 'username' => Yii::$app->request->getAuthUser(), 90 | 'password' => Yii::$app->request->getAuthPassword() 91 | ]; 92 | } 93 | 94 | public function action404() 95 | { 96 | throw new HttpException(404); 97 | } 98 | 99 | public function actionGeneralException() 100 | { 101 | throw new \Exception("General Exception"); 102 | } 103 | 104 | public function actionQuery($test) { 105 | $response = Yii::$app->response; 106 | $response->format = Response::FORMAT_JSON; 107 | return [ 108 | 'test' => $test, 109 | 'q' => Yii::$app->request->get('q'), 110 | 'queryParams' => Yii::$app->request->getQueryParams() 111 | ]; 112 | } 113 | 114 | public function actionStream() { 115 | $response = Yii::$app->response; 116 | $response->format = Response::FORMAT_RAW; 117 | if ($stream = fopen(__DIR__ . '/../.rr.yaml', 'r')) { 118 | return $response->sendStreamAsFile($stream, '.rr.yaml', [ 119 | 'mimeType' => 'text/yaml', 120 | ]); 121 | } 122 | } 123 | 124 | public function actionFile() { 125 | $response = Yii::$app->response; 126 | $response->format = Response::FORMAT_RAW; 127 | return $response->sendFile(__DIR__ . '/../.rr.yaml', '.rr.yaml', [ 128 | 'mimeType' => 'text/yaml', 129 | ]); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/rr-worker.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | waitRequest()) { 31 | if (($request instanceof Psr\Http\Message\ServerRequestInterface)) { 32 | try { 33 | $response = $application->handle($request); 34 | $psr7->respond($response); 35 | } catch (\Throwable $e) { 36 | $psr7->getWorker()->error((string)$e); 37 | } 38 | 39 | if ($application->clean()) { 40 | $psr7->getWorker()->stop(); 41 | return; 42 | } 43 | } 44 | } 45 | } catch (\Throwable $e) { 46 | $psr7->getWorker()->error((string)$e); 47 | } --------------------------------------------------------------------------------