RequestHandler::send()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Http\Server\Handler;
15
16
use Override;
0 ignored issues
show
Bug introduced by
The type Override was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use Throwable;
18
use Valkyrja\Container\Manager\Container;
19
use Valkyrja\Container\Manager\Contract\ContainerContract;
20
use Valkyrja\Http\Message\Enum\StatusCode;
21
use Valkyrja\Http\Message\Request\Contract\ServerRequestContract;
22
use Valkyrja\Http\Message\Response\Contract\ResponseContract;
23
use Valkyrja\Http\Message\Response\Response as HttpResponse;
0 ignored issues
show
Bug introduced by
The type Valkyrja\Http\Message\Response\Response was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use Valkyrja\Http\Message\Stream\Stream;
25
use Valkyrja\Http\Message\Throwable\Exception\HttpException;
26
use Valkyrja\Http\Middleware\Handler\Contract\RequestReceivedHandlerContract;
27
use Valkyrja\Http\Middleware\Handler\Contract\SendingResponseHandlerContract;
28
use Valkyrja\Http\Middleware\Handler\Contract\TerminatedHandlerContract;
29
use Valkyrja\Http\Middleware\Handler\Contract\ThrowableCaughtHandlerContract;
30
use Valkyrja\Http\Middleware\Handler\RequestReceivedHandler;
31
use Valkyrja\Http\Middleware\Handler\SendingResponseHandler;
32
use Valkyrja\Http\Middleware\Handler\TerminatedHandler;
33
use Valkyrja\Http\Middleware\Handler\ThrowableCaughtHandler;
34
use Valkyrja\Http\Routing\Dispatcher\Contract\RouterContract;
35
use Valkyrja\Http\Routing\Dispatcher\Router;
36
use Valkyrja\Http\Server\Handler\Contract\RequestHandlerContract as Contract;
37
38
use function count;
39
use function defined;
40
use function fastcgi_finish_request;
41
use function function_exists;
42
use function in_array;
43
use function litespeed_finish_request;
0 ignored issues
show
introduced by
The function litespeed_finish_request was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
44
use function ob_end_clean;
45
use function ob_end_flush;
46
use function ob_get_status;
47
use function session_id;
48
use function session_write_close;
49
50
use const PHP_OUTPUT_HANDLER_CLEANABLE;
51
use const PHP_OUTPUT_HANDLER_FLUSHABLE;
52
use const PHP_OUTPUT_HANDLER_REMOVABLE;
53
use const PHP_SAPI;
54
55
class RequestHandler implements Contract
56
{
57
    public function __construct(
58
        protected ContainerContract $container = new Container(),
59
        protected RouterContract $router = new Router(),
60
        protected RequestReceivedHandlerContract $requestReceivedHandler = new RequestReceivedHandler(),
61
        protected ThrowableCaughtHandlerContract $throwableCaughtHandler = new ThrowableCaughtHandler(),
62
        protected SendingResponseHandlerContract $sendingResponseHandler = new SendingResponseHandler(),
63
        protected TerminatedHandlerContract $terminatedHandler = new TerminatedHandler(),
64
        protected bool $debug = false
65
    ) {
66
    }
67
68
    /**
69
     * @inheritDoc
70
     *
71
     * @throws Throwable
72
     */
73
    #[Override]
74
    public function handle(ServerRequestContract $request): ResponseContract
75
    {
76
        try {
77
            $response = $this->dispatchRouter($request);
78
        } catch (Throwable $throwable) {
79
            $response = $this->getResponseFromThrowable($throwable);
80
            $response = $this->throwableCaughtHandler->throwableCaught($request, $response, $throwable);
81
        }
82
83
        // Set the returned response in the container
84
        $this->container->setSingleton(ResponseContract::class, $response);
85
86
        return $response;
87
    }
88
89
    /**
90
     * @inheritDoc
91
     */
92
    #[Override]
93
    public function send(ResponseContract $response): static
94
    {
95
        $response->send();
96
97
        $this->finishSession();
98
        $this->finishRequest();
99
100
        return $this;
101
    }
102
103
    /**
104
     * @inheritDoc
105
     */
106
    #[Override]
107
    public function terminate(ServerRequestContract $request, ResponseContract $response): void
108
    {
109
        // Dispatch the terminable middleware
110
        $this->terminatedHandler->terminated($request, $response);
111
    }
112
113
    /**
114
     * @inheritDoc
115
     *
116
     * @throws Throwable
117
     */
118
    #[Override]
119
    public function run(ServerRequestContract $request): void
120
    {
121
        // Handle the request, dispatch the after request middleware
122
        $response = $this->handle($request);
123
        // Dispatch the sending middleware
124
        $response = $this->sendingResponseHandler->sendingResponse($request, $response);
125
126
        // Set the returned response in the container
127
        $this->container->setSingleton(ResponseContract::class, $response);
128
129
        // Send the response
130
        $this->send($response);
131
        // Terminate the application
132
        $this->terminate($request, $response);
133
    }
134
135
    /**
136
     * Dispatch the request via the router.
137
     *
138
     * @param ServerRequestContract $request The request
139
     *
140
     * @return ResponseContract
141
     */
142
    protected function dispatchRouter(ServerRequestContract $request): ResponseContract
143
    {
144
        // Set the request object in the container
145
        $this->container->setSingleton(ServerRequestContract::class, $request);
146
147
        // Dispatch the before request handled middleware
148
        $requestAfterMiddleware = $this->requestReceivedHandler->requestReceived($request);
149
150
        // If the return value after middleware is a response return it
151
        if ($requestAfterMiddleware instanceof ResponseContract) {
152
            return $requestAfterMiddleware;
153
        }
154
155
        // Set the returned request in the container
156
        $this->container->setSingleton(ServerRequestContract::class, $requestAfterMiddleware);
157
158
        return $this->router->dispatch($requestAfterMiddleware);
159
    }
160
161
    /**
162
     * Get a response from a throwable.
163
     *
164
     * @param Throwable $throwable The exception
165
     *
166
     * @throws Throwable
167
     *
168
     * @return ResponseContract
169
     */
170
    protected function getResponseFromThrowable(Throwable $throwable): ResponseContract
171
    {
172
        if ($this->debug) {
173
            throw $throwable;
174
        }
175
176
        // If no response has been set and there is a template with the error code
177
        if ($throwable instanceof HttpException) {
178
            return $throwable->getResponse()
179
                ?? $this->getDefaultErrorResponse($throwable);
180
        }
181
182
        return $this->getDefaultErrorResponse();
183
    }
184
185
    /**
186
     * Get the default exception response.
187
     *
188
     * @param HttpException|null $httpException [optional] The Http exception
189
     *
190
     * @return ResponseContract
191
     */
192
    protected function getDefaultErrorResponse(HttpException|null $httpException = null): ResponseContract
193
    {
194
        $statusCode = StatusCode::INTERNAL_SERVER_ERROR;
195
196
        $body = new Stream();
197
        $body->write('Unknown Server Error Occurred');
198
        $body->rewind();
199
200
        if ($httpException !== null) {
201
            $statusCode = $httpException->getStatusCode();
202
            $body->write('Unknown Server Error Occurred - ' . $httpException->getTraceCode());
203
            $body->rewind();
204
        }
205
206
        return new HttpResponse(
207
            body: $body,
208
            statusCode: $statusCode
209
        );
210
    }
211
212
    /**
213
     * Finish a session if it is active.
214
     *
215
     * @return void
216
     */
217
    protected function finishSession(): void
218
    {
219
        if ($this->shouldCloseSession()) {
220
            $this->closeSession();
221
        }
222
    }
223
224
    /**
225
     * Determine if the session should be closed.
226
     *
227
     * @return bool
228
     */
229
    protected function shouldCloseSession(): bool
230
    {
231
        $sessionId = session_id();
232
233
        return $sessionId !== false && $sessionId !== '';
234
    }
235
236
    /**
237
     * Close the session.
238
     *
239
     * @return void
240
     */
241
    protected function closeSession(): void
242
    {
243
        session_write_close();
244
    }
245
246
    /**
247
     * Finish the request.
248
     *
249
     * @return void
250
     */
251
    protected function finishRequest(): void
252
    {
253
        // If fastcgi is enabled
254
        if ($this->shouldUseFastcgiToFinishRequest()) {
255
            // Use it to finish the request
256
            $this->finishRequestWithFastcgi();
257
        } elseif ($this->shouldUseLitespeedToFinishRequest()) {
258
            // If litespeed is enabled
259
            // Use it to finish the request
260
            $this->finishRequestWithLitespeed();
261
        } elseif ($this->shouldCloseOutputBuffersToFinishRequest()) {
262
            // Otherwise if this isn't a cli request
263
            // Use an internal method to finish the request
264
            $this->closeOutputBuffers(0, true);
265
        } else {
266
            $this->finishRequestForAllOtherTypes();
267
        }
268
    }
269
270
    /**
271
     * Determine if the request should be finished with Fastcgi.
272
     *
273
     * @return bool
274
     */
275
    protected function shouldUseFastcgiToFinishRequest(): bool
276
    {
277
        return function_exists('fastcgi_finish_request');
278
    }
279
280
    /**
281
     * Determine if the request should be finished with Litespeed.
282
     *
283
     * @return bool
284
     */
285
    protected function shouldUseLitespeedToFinishRequest(): bool
286
    {
287
        return function_exists('litespeed_finish_request');
288
    }
289
290
    /**
291
     * Determine if the request should be finished via closing the output buffers.
292
     *
293
     * @return bool
294
     */
295
    protected function shouldCloseOutputBuffersToFinishRequest(): bool
296
    {
297
        return ! in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true);
298
    }
299
300
    /**
301
     * Finish the request with Fastcgi.
302
     *
303
     * @return void
304
     */
305
    protected function finishRequestWithFastcgi(): void
306
    {
307
        fastcgi_finish_request();
308
    }
309
310
    /**
311
     * Finish the request with Litespeed.
312
     *
313
     * @return void
314
     */
315
    protected function finishRequestWithLitespeed(): void
316
    {
317
        /** @psalm-suppress UndefinedFunction */
318
        litespeed_finish_request();
0 ignored issues
show
Bug introduced by
The function litespeed_finish_request was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

318
        /** @scrutinizer ignore-call */ 
319
        litespeed_finish_request();
Loading history...
319
    }
320
321
    /**
322
     * Cleans or flushes output buffers up to target level.
323
     * Resulting level can be greater than target level if a non-removable
324
     * buffer has been encountered.
325
     *
326
     * @param int  $targetLevel The target output buffering level
327
     * @param bool $flush       Whether to flush or clean the buffers
328
     *
329
     * @return void
330
     */
331
    protected function closeOutputBuffers(int $targetLevel, bool $flush): void
332
    {
333
        $status = $this->outputBuffersGetStatus();
334
        $level  = count($status);
335
336
        $flushOrCleanFlag = $flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE;
337
        // PHP_OUTPUT_HANDLER_* are not defined on HHVM 3.3
338
        $flags = defined('PHP_OUTPUT_HANDLER_REMOVABLE') ? PHP_OUTPUT_HANDLER_REMOVABLE | $flushOrCleanFlag : -1;
339
340
        while (
341
            $level-- > $targetLevel
342
            && ($s = $status[$level])
343
            && ($s['del'] ?? (! isset($s['flags']) || $flags === ($s['flags'] & $flags)))
344
        ) {
345
            if ($flush) {
346
                $this->closeOutputBuffersWithFlush();
347
            } else {
348
                $this->closeOutputBuffersWithClean();
349
            }
350
        }
351
    }
352
353
    /**
354
     * Get the status of the output buffers.
355
     *
356
     * @return array<int, ?array{chunk_size: int, buffer_size: int, buffer_used: int, flags?: int, level?: int, type?: int, del?: int, name?: string}>
357
     *
358
     * @psalm-suppress MixedReturnTypeCoercion
359
     */
360
    protected function outputBuffersGetStatus(): array
361
    {
362
        return ob_get_status(true);
363
    }
364
365
    /**
366
     * End the output buffers with flush.
367
     *
368
     * @return void
369
     */
370
    protected function closeOutputBuffersWithFlush(): void
371
    {
372
        ob_end_flush();
373
    }
374
375
    /**
376
     * End the output buffers with clean.
377
     *
378
     * @return void
379
     */
380
    protected function closeOutputBuffersWithClean(): void
381
    {
382
        ob_end_clean();
383
    }
384
385
    /**
386
     * Finish the request for any scenario not previously caught.
387
     *
388
     * @return void
389
     */
390
    protected function finishRequestForAllOtherTypes(): void
391
    {
392
    }
393
}
394