Application::defaultRequestFactory()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 12
cts 12
cp 1
rs 9.7666
c 0
b 0
f 0
cc 1
crap 1
nc 1
nop 1
1
<?php declare(strict_types=1);
2
3
namespace Limoncello\Core\Application;
4
5
/**
6
 * Copyright 2015-2020 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use Closure;
22
use Laminas\Diactoros\Response\EmptyResponse;
23
use Laminas\Diactoros\Response\TextResponse;
24
use Laminas\Diactoros\ServerRequest;
25
use Limoncello\Common\Reflection\CheckCallableTrait;
26
use Limoncello\Contracts\Container\ContainerInterface as LimoncelloContainerInterface;
27
use Limoncello\Contracts\Core\ApplicationInterface;
28
use Limoncello\Contracts\Core\SapiInterface;
29
use Limoncello\Contracts\Exceptions\ThrowableHandlerInterface;
30
use Limoncello\Contracts\Http\ThrowableResponseInterface;
31
use Limoncello\Contracts\Routing\RouterInterface;
32
use Limoncello\Core\Routing\Router;
33
use LogicException;
34
use Psr\Container\ContainerExceptionInterface;
35
use Psr\Container\ContainerInterface as PsrContainerInterface;
36
use Psr\Http\Message\RequestInterface;
37
use Psr\Http\Message\ResponseInterface;
38
use Psr\Http\Message\ServerRequestInterface;
39
use ReflectionException;
40
use Throwable;
41
use function assert;
42
use function call_user_func;
43
use function count;
44
use function implode;
45
46
/**
47
 * @package Limoncello\Core
48
 *
49
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
50
 */
51
abstract class Application implements ApplicationInterface
52
{
53
    use CheckCallableTrait;
54
55
    /** Method name for default request factory. */
56
    const FACTORY_METHOD = 'defaultRequestFactory';
57
58
    /** HTTP error code for default error response. */
59
    protected const DEFAULT_HTTP_ERROR_CODE = 500;
60
61
    /**
62
     * @var SapiInterface|null
63
     */
64
    private $sapi;
65
66
    /**
67
     * @var RouterInterface|null
68
     */
69
    private $router = null;
70
71
    /**
72
     * @return array
73
     */
74
    abstract protected function getCoreData(): array;
75
76
    /**
77
     * @return LimoncelloContainerInterface
78
     */
79
    abstract protected function createContainerInstance(): LimoncelloContainerInterface;
80
81
    /**
82
     * @inheritdoc
83
     */
84 8
    public function setSapi(SapiInterface $sapi): ApplicationInterface
85
    {
86 8
        $this->sapi = $sapi;
87
88 8
        return $this;
89
    }
90
91
    /**
92
     * @inheritdoc
93
     *
94
     * @SuppressWarnings(PHPMD.StaticAccess)
95
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
96
     */
97 9
    public function run(): void
98
    {
99 9
        if ($this->sapi === null) {
100 1
            throw new LogicException('SAPI not set.');
101
        }
102
103 8
        $container = null;
104
105
        try {
106 8
            $coreData = $this->getCoreData();
107
108
            // match route from `Request` to handler, route container configurators/middleware, etc
109
            list($matchCode, $allowedMethods, $handlerParams, $handler,
110 8
                $routeMiddleware, $routeConfigurators, $requestFactory) = $this->initRouter($coreData)
111 8
                ->match($this->sapi->getMethod(), $this->sapi->getUri()->getPath());
112
113
            // configure container
114 8
            $container = $this->createContainerInstance();
115 8
            $globalConfigurators = BaseCoreData::getGlobalConfiguratorsFromData($coreData);
116 8
            $this->configureContainer($container, $globalConfigurators, $routeConfigurators);
117
118
            // build pipeline for handling `Request`: global middleware -> route middleware -> handler (e.g. controller)
119
120
            // select terminal handler
121
            switch ($matchCode) {
122 8
                case RouterInterface::MATCH_FOUND:
123 6
                    $handler = $this->createHandler($handler, $handlerParams, $container);
124 6
                    break;
125 2
                case RouterInterface::MATCH_METHOD_NOT_ALLOWED:
126 1
                    $handler = $this->createMethodNotAllowedTerminalHandler($allowedMethods);
127 1
                    break;
128
                default:
129 1
                    assert($matchCode === RouterInterface::MATCH_NOT_FOUND);
130 1
                    $handler = $this->createNotFoundTerminalHandler();
131 1
                    break;
132
            }
133
134 8
            $globalMiddleware = BaseCoreData::getGlobalMiddlewareFromData($coreData);
135 8
            $hasMiddleware    = empty($globalMiddleware) === false || empty($routeMiddleware) === false;
136
137 8
            $handler = $hasMiddleware === true ?
138 8
                $this->addMiddlewareChain($handler, $container, $globalMiddleware, $routeMiddleware) : $handler;
139
140
            $request =
141 8
                $requestFactory === null && $hasMiddleware === false && $matchCode === RouterInterface::MATCH_FOUND ?
142 1
                null :
143 8
                $this->createRequest($this->sapi, $container, $requestFactory ?? static::getDefaultRequestFactory());
144
145
            // Execute the pipeline by sending `Request` down all middleware (global then route's then
146
            // terminal handler in `Controller` and back) and then send `Response` to SAPI
147 8
            $this->sapi->handleResponse($this->handleRequest($handler, $request));
148 1
        } catch (Throwable $throwable) {
149 1
            $this->sapi->handleResponse($this->handleThrowable($throwable, $container));
150
        }
151
    }
152
153
    /**
154
     * @return callable
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string[].

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
155
     */
156 8
    public static function getDefaultRequestFactory(): callable
157
    {
158 8
        return [static::class, static::FACTORY_METHOD];
159
    }
160
161
    /**
162
     * @param SapiInterface $sapi
163
     *
164
     * @return ServerRequestInterface
165
     */
166 6
    public static function defaultRequestFactory(SapiInterface $sapi): ServerRequestInterface
167
    {
168 6
        return new ServerRequest(
169 6
            $sapi->getServer(),
170 6
            $sapi->getFiles(),
171 6
            $sapi->getUri(),
172 6
            $sapi->getMethod(),
173 6
            $sapi->getRequestBody(),
174 6
            $sapi->getHeaders(),
175 6
            $sapi->getCookies(),
176 6
            $sapi->getQueryParams(),
177 6
            $sapi->getParsedBody(),
178 6
            $sapi->getProtocolVersion()
179
        );
180
    }
181
182
    /**
183
     * @param Closure               $handler
184
     * @param RequestInterface|null $request
185
     *
186
     * @return ResponseInterface
187
     */
188 8
    protected function handleRequest(Closure $handler, ?RequestInterface $request): ResponseInterface
189
    {
190 8
        $response = call_user_func($handler, $request);
191
192 7
        return $response;
193
    }
194
195
    /**
196
     * @param Throwable                  $throwable
197
     * @param null|PsrContainerInterface $container
198
     *
199
     * @return ThrowableResponseInterface
200
     *
201
     * @throws ContainerExceptionInterface
202
     *
203
     * @SuppressWarnings(PHPMD.ElseExpression)
204
     */
205 2
    protected function handleThrowable(
206
        Throwable $throwable,
207
        ?PsrContainerInterface $container
208
    ): ThrowableResponseInterface {
209 2
        if ($container !== null && $container->has(ThrowableHandlerInterface::class) === true) {
210
            /** @var ThrowableHandlerInterface $handler */
211
            /** @noinspection PhpUnhandledExceptionInspection */
212 1
            $handler  = $container->get(ThrowableHandlerInterface::class);
213 1
            $response = $handler->createResponse($throwable, $container);
214
        } else {
215 2
            $response = $this->createDefaultThrowableResponse($throwable);
216
        }
217
218 2
        return $response;
219
    }
220
221
    /**
222
     * @param int   $status
223
     * @param array $headers
224
     *
225
     * @return ResponseInterface
226
     */
227 2
    protected function createEmptyResponse($status = 204, array $headers = []): ResponseInterface
228
    {
229 2
        $response = new EmptyResponse($status, $headers);
230
231 2
        return $response;
232
    }
233
234
    /**
235
     * @param Throwable $throwable
236
     *
237
     * @return ThrowableResponseInterface
238
     */
239 2
    protected function createDefaultThrowableResponse(Throwable $throwable): ThrowableResponseInterface
240
    {
241 2
        $status   = static::DEFAULT_HTTP_ERROR_CODE;
242
        $response = new class ($throwable, $status) extends TextResponse implements ThrowableResponseInterface
243
        {
244
            use ThrowableResponseTrait;
245
246
            /**
247
             * @param Throwable $throwable
248
             * @param int       $status
249
             */
250 2
            public function __construct(Throwable $throwable, int $status)
251
            {
252 2
                parent::__construct((string)$throwable, $status);
253 2
                $this->setThrowable($throwable);
254
            }
255
        };
256
257 2
        return $response;
258
    }
259
260
    /**
261
     * @return RouterInterface|null
262
     */
263 1
    protected function getRouter(): ?RouterInterface
264
    {
265 1
        return $this->router;
266
    }
267
268
    /**
269
     * @param LimoncelloContainerInterface $container
270
     * @param callable[]|null              $globalConfigurators
271
     * @param callable[]|null              $routeConfigurators
272
     *
273
     * @return void
274
     *
275
     * @throws ReflectionException
276
     */
277 8
    protected function configureContainer(
278
        LimoncelloContainerInterface $container,
279
        array $globalConfigurators = null,
280
        array $routeConfigurators = null
281
    ): void {
282 8
        if (empty($globalConfigurators) === false) {
283 8
            foreach ($globalConfigurators as $configurator) {
284 8
                assert($this->checkPublicStaticCallable($configurator, [LimoncelloContainerInterface::class]));
285 8
                $configurator($container);
286
            }
287
        }
288 8
        if (empty($routeConfigurators) === false) {
289 3
            foreach ($routeConfigurators as $configurator) {
290 3
                assert($this->checkPublicStaticCallable($configurator, [LimoncelloContainerInterface::class]));
291 3
                $configurator($container);
292
            }
293
        }
294
    }
295
296
    /**
297
     * @param Closure               $handler
298
     * @param PsrContainerInterface $container
299
     * @param array|null            $globalMiddleware
300
     * @param array|null            $routeMiddleware
301
     *
302
     * @return Closure
303
     *
304
     * @throws ReflectionException
305
     */
306 6
    protected function addMiddlewareChain(
307
        Closure $handler,
308
        PsrContainerInterface $container,
309
        array $globalMiddleware,
310
        array $routeMiddleware = null
311
    ): Closure {
312 6
        $handler = $this->createMiddlewareChainImpl($handler, $container, $routeMiddleware);
313 6
        $handler = $this->createMiddlewareChainImpl($handler, $container, $globalMiddleware);
314
315 6
        return $handler;
316
    }
317
318
    /**
319
     * @param callable                    $handler
320
     * @param array                       $handlerParams
321
     * @param PsrContainerInterface       $container
322
     * @param ServerRequestInterface|null $request
323
     *
324
     * @return ResponseInterface
325
     *
326
     * @throws ReflectionException
327
     */
328 5
    protected function callHandler(
329
        callable $handler,
330
        array $handlerParams,
331
        PsrContainerInterface $container,
332
        ServerRequestInterface $request = null
333
    ): ResponseInterface {
334
        // check the handler method signature
335 5
        assert(
336 5
            $this->checkPublicStaticCallable(
337 5
                $handler,
338 5
                ['array', PsrContainerInterface::class, ServerRequestInterface::class],
339 5
                ResponseInterface::class
340
            ),
341
            'Handler method should have signature ' .
342 5
            '`public static methodName(array, PsrContainerInterface, ServerRequestInterface): ResponseInterface`'
343
        );
344
345 5
        $response = call_user_func($handler, $handlerParams, $container, $request);
346
347 4
        return $response;
348
    }
349
350
    /**
351
     * @param array $coreData
352
     *
353
     * @return RouterInterface
354
     *
355
     * @SuppressWarnings(PHPMD.StaticAccess)
356
     */
357 8
    protected function initRouter(array $coreData): RouterInterface
358
    {
359 8
        $routerParams    = BaseCoreData::getRouterParametersFromData($coreData);
360 8
        $routesData      = BaseCoreData::getRoutesDataFromData($coreData);
361 8
        $generatorClass  = BaseCoreData::getGeneratorFromParametersData($routerParams);
362 8
        $dispatcherClass = BaseCoreData::getDispatcherFromParametersData($routerParams);
363
364 8
        $this->router = new Router($generatorClass, $dispatcherClass);
365 8
        $this->router->loadCachedRoutes($routesData);
366
367 8
        return $this->router;
368
    }
369
370
    /**
371
     * @param callable              $handler
372
     * @param array                 $handlerParams
373
     * @param PsrContainerInterface $container
374
     *
375
     * @return Closure
376
     */
377 6
    protected function createHandler(
378
        callable $handler,
379
        array $handlerParams,
380
        PsrContainerInterface $container
381
    ): Closure {
382
        return function (ServerRequestInterface $request = null) use (
383 5
            $handler,
384 5
            $handlerParams,
385 5
            $container
386
        ): ResponseInterface {
387
            try {
388 5
                return $this->callHandler($handler, $handlerParams, $container, $request);
389 1
            } catch (Throwable $throwable) {
390 1
                return $this->handleThrowable($throwable, $container);
391
            }
392 6
        };
393
    }
394
395
    /**
396
     * @param SapiInterface         $sapi
397
     * @param PsrContainerInterface $container
398
     * @param callable              $requestFactory
399
     *
400
     * @return ServerRequestInterface
401
     *
402
     * @throws ReflectionException
403
     */
404 7
    private function createRequest(
405
        SapiInterface $sapi,
406
        PsrContainerInterface $container,
407
        callable $requestFactory
408
    ): ServerRequestInterface {
409
        // check the factory method signature
410 7
        assert(
411 7
            $this->checkPublicStaticCallable(
412 7
                $requestFactory,
413 7
                [SapiInterface::class, PsrContainerInterface::class],
414 7
                ServerRequestInterface::class
415
            ),
416
            'Factory method should have signature ' .
417 7
            '`public static methodName(SapiInterface, PsrContainerInterface): ServerRequestInterface`'
418
        );
419
420 7
        $request = call_user_func($requestFactory, $sapi, $container);
421
422 7
        return $request;
423
    }
424
425
    /**
426
     * @param array $allowedMethods
427
     *
428
     * @return Closure
429
     */
430 1
    private function createMethodNotAllowedTerminalHandler(array $allowedMethods): Closure
431
    {
432
        // 405 Method Not Allowed
433
        return function () use ($allowedMethods): ResponseInterface {
434 1
            return $this->createEmptyResponse(405, ['Accept' => implode(',', $allowedMethods)]);
435 1
        };
436
    }
437
438
    /**
439
     * @return Closure
440
     */
441 1
    private function createNotFoundTerminalHandler(): Closure
442
    {
443
        // 404 Not Found
444
        return function (): ResponseInterface {
445 1
            return $this->createEmptyResponse(404);
446 1
        };
447
    }
448
449
    /**
450
     * @param Closure               $handler
451
     * @param PsrContainerInterface $container
452
     * @param array|null            $middleware
453
     *
454
     * @return Closure
455
     *
456
     * @throws ReflectionException
457
     */
458 6
    private function createMiddlewareChainImpl(
459
        Closure $handler,
460
        PsrContainerInterface $container,
461
        array $middleware = null
462
    ): Closure {
463 6
        if (empty($middleware) === false) {
464 6
            $start = count($middleware) - 1;
465 6
            for ($index = $start; $index >= 0; $index--) {
466 6
                $handler = $this->createMiddlewareChainLink($handler, $middleware[$index], $container);
467
            }
468
        }
469
470 6
        return $handler;
471
    }
472
473
    /**
474
     * @param Closure               $next
475
     * @param callable              $middleware
476
     * @param PsrContainerInterface $container
477
     *
478
     * @return Closure
479
     *
480
     * @throws ReflectionException
481
     */
482 6
    private function createMiddlewareChainLink(
483
        Closure $next,
484
        callable $middleware,
485
        PsrContainerInterface $container
486
    ): Closure {
487
        // check the middleware method signature
488 6
        assert(
489 6
            $this->checkPublicStaticCallable(
490 6
                $middleware,
491 6
                [ServerRequestInterface::class, Closure::class, PsrContainerInterface::class],
492 6
                ResponseInterface::class
493
            ),
494
            'Middleware method should have signature ' .
495 6
            '`public static methodName(ServerRequestInterface, Closure, PsrContainerInterface): ResponseInterface`'
496
        );
497
498
        return function (ServerRequestInterface $request) use ($next, $middleware, $container): ResponseInterface {
499 6
            return call_user_func($middleware, $request, $next, $container);
500 6
        };
501
    }
502
}
503