RequestHandler   A
last analyzed

Complexity

Total Complexity 39

Size/Duplication

Total Lines 340
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 39
eloc 77
dl 0
loc 340
rs 9.28
c 0
b 0
f 0

22 Methods

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

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