Completed
Push — master ( 55506b...17b5bf )
by Neomerx
04:16
created

Application::createHandler()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 17
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 8
cts 8
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 14
nc 1
nop 3
crap 2
1
<?php namespace Limoncello\Core\Application;
2
3
/**
4
 * Copyright 2015-2017 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Closure;
20
use Limoncello\Contracts\Container\ContainerInterface as LimoncelloContainerInterface;
21
use Limoncello\Contracts\Core\ApplicationInterface;
22
use Limoncello\Contracts\Core\SapiInterface;
23
use Limoncello\Contracts\Exceptions\ThrowableHandlerInterface;
24
use Limoncello\Contracts\Http\ThrowableResponseInterface;
25
use Limoncello\Contracts\Routing\RouterInterface;
26
use Limoncello\Core\Reflection\CheckCallableTrait;
27
use Limoncello\Core\Routing\Router;
28
use LogicException;
29
use Psr\Container\ContainerExceptionInterface;
30
use Psr\Container\ContainerInterface as PsrContainerInterface;
31
use Psr\Http\Message\RequestInterface;
32
use Psr\Http\Message\ResponseInterface;
33
use Psr\Http\Message\ServerRequestInterface;
34
use Throwable;
35
use Zend\Diactoros\Response\EmptyResponse;
36
use Zend\Diactoros\Response\TextResponse;
37
use Zend\Diactoros\ServerRequest;
38
39
/**
40
 * @package Limoncello\Core
41
 *
42
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
43
 */
44
abstract class Application implements ApplicationInterface
45
{
46
    use CheckCallableTrait;
47
48
    /** Method name for default request factory. */
49
    const FACTORY_METHOD = 'defaultRequestFactory';
50
51
    /** HTTP error code for default error response. */
52
    protected const DEFAULT_HTTP_ERROR_CODE = 500;
53
54
    /**
55
     * @var SapiInterface|null
56
     */
57
    private $sapi;
58
59
    /**
60
     * @var RouterInterface|null
61
     */
62
    private $router = null;
63
64
    /**
65
     * @return array
66
     */
67
    abstract protected function getCoreData(): array;
68
69
    /**
70
     * @return LimoncelloContainerInterface
71
     */
72
    abstract protected function createContainerInstance(): LimoncelloContainerInterface;
73
74
    /**
75
     * @inheritdoc
76
     */
77 8
    public function setSapi(SapiInterface $sapi): ApplicationInterface
78
    {
79 8
        $this->sapi = $sapi;
80
81 8
        return $this;
82
    }
83
84
    /**
85
     * @inheritdoc
86
     *
87
     * @SuppressWarnings(PHPMD.StaticAccess)
88
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
89
     */
90 9
    public function run(): void
91
    {
92 9
        if ($this->sapi === null) {
93 1
            throw new LogicException('SAPI not set.');
94
        }
95
96 8
        $container = null;
97
98
        try {
99 8
            $coreData = $this->getCoreData();
100
101
            // match route from `Request` to handler, route container configurators/middleware, etc
102
            list($matchCode, $allowedMethods, $handlerParams, $handler,
103 8
                $routeMiddleware, $routeConfigurators, $requestFactory) = $this->initRouter($coreData)
104 8
                ->match($this->sapi->getMethod(), $this->sapi->getUri()->getPath());
105
106
            // configure container
107 8
            $container = $this->createContainerInstance();
108 8
            $globalConfigurators = BaseCoreData::getGlobalConfiguratorsFromData($coreData);
109 8
            $this->configureContainer($container, $globalConfigurators, $routeConfigurators);
110
111
            // build pipeline for handling `Request`: global middleware -> route middleware -> handler (e.g. controller)
112
113
            // select terminal handler
114
            switch ($matchCode) {
115 8
                case RouterInterface::MATCH_FOUND:
116 6
                    $handler = $this->createHandler($handler, $handlerParams, $container);
117 6
                    break;
118 2
                case RouterInterface::MATCH_METHOD_NOT_ALLOWED:
119 1
                    $handler = $this->createMethodNotAllowedTerminalHandler($allowedMethods);
120 1
                    break;
121
                default:
122 1
                    assert($matchCode === RouterInterface::MATCH_NOT_FOUND);
123 1
                    $handler = $this->createNotFoundTerminalHandler();
124 1
                    break;
125
            }
126
127 8
            $globalMiddleware = BaseCoreData::getGlobalMiddlewareFromData($coreData);
128 8
            $hasMiddleware    = empty($globalMiddleware) === false || empty($routeMiddleware) === false;
129
130 8
            $handler = $hasMiddleware === true ?
131 8
                $this->addMiddlewareChain($handler, $container, $globalMiddleware, $routeMiddleware) : $handler;
132
133
            $request =
134 8
                $requestFactory === null && $hasMiddleware === false && $matchCode === RouterInterface::MATCH_FOUND ?
135 1
                null :
136 8
                $this->createRequest($this->sapi, $container, $requestFactory ?? static::getDefaultRequestFactory());
137
138
            // Execute the pipeline by sending `Request` down all middleware (global then route's then
139
            // terminal handler in `Controller` and back) and then send `Response` to SAPI
140 8
            $this->sapi->handleResponse($this->handleRequest($handler, $request));
141 1
        } catch (Throwable $throwable) {
142 1
            $this->sapi->handleResponse($this->handleThrowable($throwable, $container));
143
        }
144
    }
145
146
    /**
147
     * @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...
148
     */
149 8
    public static function getDefaultRequestFactory(): callable
150
    {
151 8
        return [static::class, static::FACTORY_METHOD];
152
    }
153
154
    /**
155
     * @param SapiInterface $sapi
156
     *
157
     * @return ServerRequestInterface
158
     */
159 6
    public static function defaultRequestFactory(SapiInterface $sapi): ServerRequestInterface
160
    {
161 6
        return new ServerRequest(
162 6
            $sapi->getServer(),
163 6
            $sapi->getFiles(),
164 6
            $sapi->getUri(),
165 6
            $sapi->getMethod(),
166 6
            $sapi->getRequestBody(),
167 6
            $sapi->getHeaders(),
168 6
            $sapi->getCookies(),
169 6
            $sapi->getQueryParams(),
170 6
            $sapi->getParsedBody(),
171 6
            $sapi->getProtocolVersion()
172
        );
173
    }
174
175
    /**
176
     * @param Closure               $handler
177
     * @param RequestInterface|null $request
178
     *
179
     * @return ResponseInterface
180
     */
181 8
    protected function handleRequest(Closure $handler, ?RequestInterface $request): ResponseInterface
182
    {
183 8
        $response = call_user_func($handler, $request);
184
185 7
        return $response;
186
    }
187
188
    /**
189
     * @param Throwable                  $throwable
190
     * @param null|PsrContainerInterface $container
191
     *
192
     * @return ThrowableResponseInterface
193
     *
194
     * @throws ContainerExceptionInterface
195
     */
196 2
    protected function handleThrowable(
197
        Throwable $throwable,
198
        ?PsrContainerInterface $container
199
    ): ThrowableResponseInterface {
200 2
        if ($container !== null && $container->has(ThrowableHandlerInterface::class) === true) {
201
            /** @var ThrowableHandlerInterface $handler */
202
            /** @noinspection PhpUnhandledExceptionInspection */
203 1
            $handler  = $container->get(ThrowableHandlerInterface::class);
204 1
            $response = $handler->createResponse($throwable, $container);
205
        } else {
206 2
            $response = $this->createDefaultThrowableResponse($throwable);
207
        }
208
209 2
        return $response;
210
    }
211
212
    /**
213
     * @param int   $status
214
     * @param array $headers
215
     *
216
     * @return ResponseInterface
217
     */
218 2
    protected function createEmptyResponse($status = 204, array $headers = []): ResponseInterface
219
    {
220 2
        $response = new EmptyResponse($status, $headers);
221
222 2
        return $response;
223
    }
224
225
    /**
226
     * @param Throwable $throwable
227
     *
228
     * @return ThrowableResponseInterface
229
     */
230 2
    protected function createDefaultThrowableResponse(Throwable $throwable): ThrowableResponseInterface
231
    {
232 2
        $status   = static::DEFAULT_HTTP_ERROR_CODE;
233
        $response = new class ($throwable, $status) extends TextResponse implements ThrowableResponseInterface
234
        {
235
            use ThrowableResponseTrait;
236
237
            /**
238
             * @param Throwable $throwable
239
             * @param int       $status
240
             */
241 2
            public function __construct(Throwable $throwable, int $status)
242
            {
243 2
                parent::__construct((string)$throwable, $status);
244 2
                $this->setThrowable($throwable);
245
            }
246
        };
247
248 2
        return $response;
249
    }
250
251
    /**
252
     * @return RouterInterface|null
253
     */
254 1
    protected function getRouter(): ?RouterInterface
255
    {
256 1
        return $this->router;
257
    }
258
259
    /**
260
     * @param LimoncelloContainerInterface $container
261
     * @param callable[]|null              $globalConfigurators
262
     * @param callable[]|null              $routeConfigurators
263
     *
264
     * @return void
265
     */
266 8
    protected function configureContainer(
267
        LimoncelloContainerInterface $container,
268
        array $globalConfigurators = null,
269
        array $routeConfigurators = null
270
    ): void {
271 8
        if (empty($globalConfigurators) === false) {
272 8
            foreach ($globalConfigurators as $configurator) {
273 8
                assert($this->checkPublicStaticCallable($configurator, [LimoncelloContainerInterface::class]));
274 8
                $configurator($container);
275
            }
276
        }
277 8
        if (empty($routeConfigurators) === false) {
278 3
            foreach ($routeConfigurators as $configurator) {
279 3
                assert($this->checkPublicStaticCallable($configurator, [LimoncelloContainerInterface::class]));
280 3
                $configurator($container);
281
            }
282
        }
283
    }
284
285
    /**
286
     * @param Closure               $handler
287
     * @param PsrContainerInterface $container
288
     * @param array|null            $globalMiddleware
289
     * @param array|null            $routeMiddleware
290
     *
291
     * @return Closure
292
     */
293 6
    protected function addMiddlewareChain(
294
        Closure $handler,
295
        PsrContainerInterface $container,
296
        array $globalMiddleware,
297
        array $routeMiddleware = null
298
    ): Closure {
299 6
        $handler = $this->createMiddlewareChainImpl($handler, $container, $routeMiddleware);
300 6
        $handler = $this->createMiddlewareChainImpl($handler, $container, $globalMiddleware);
301
302 6
        return $handler;
303
    }
304
305
    /**
306
     * @param callable                    $handler
307
     * @param array                       $handlerParams
308
     * @param PsrContainerInterface       $container
309
     * @param ServerRequestInterface|null $request
310
     *
311
     * @return ResponseInterface
312
     */
313 5
    protected function callHandler(
314
        callable $handler,
315
        array $handlerParams,
316
        PsrContainerInterface $container,
317
        ServerRequestInterface $request = null
318
    ): ResponseInterface {
319
        // check the handler method signature
320 5
        assert(
321 5
            $this->checkPublicStaticCallable(
322 5
                $handler,
323 5
                ['array', PsrContainerInterface::class, ServerRequestInterface::class],
324 5
                ResponseInterface::class
325
            ),
326
            'Handler method should have signature ' .
327 5
            '`public static methodName(array, PsrContainerInterface, ServerRequestInterface): ResponseInterface`'
328
        );
329
330 5
        $response = call_user_func($handler, $handlerParams, $container, $request);
331
332 4
        return $response;
333
    }
334
335
    /**
336
     * @param array $coreData
337
     *
338
     * @return RouterInterface
339
     *
340
     * @SuppressWarnings(PHPMD.StaticAccess)
341
     */
342 8
    protected function initRouter(array $coreData): RouterInterface
343
    {
344 8
        $routerParams    = BaseCoreData::getRouterParametersFromData($coreData);
345 8
        $routesData      = BaseCoreData::getRoutesDataFromData($coreData);
346 8
        $generatorClass  = BaseCoreData::getGeneratorFromParametersData($routerParams);
347 8
        $dispatcherClass = BaseCoreData::getDispatcherFromParametersData($routerParams);
348
349 8
        $this->router = new Router($generatorClass, $dispatcherClass);
350 8
        $this->router->loadCachedRoutes($routesData);
351
352 8
        return $this->router;
353
    }
354
355
    /**
356
     * @param callable              $handler
357
     * @param array                 $handlerParams
358
     * @param PsrContainerInterface $container
359
     *
360
     * @return Closure
361
     */
362
    protected function createHandler(
363
        callable $handler,
364
        array $handlerParams,
365
        PsrContainerInterface $container
366
    ): Closure {
367 6
        return function (ServerRequestInterface $request = null) use (
368 5
            $handler,
369 5
            $handlerParams,
370 5
            $container
371
        ): ResponseInterface {
372
            try {
373 5
                return $this->callHandler($handler, $handlerParams, $container, $request);
374 1
            } catch (Throwable $throwable) {
375 1
                return $this->handleThrowable($throwable, $container);
376
            }
377 6
        };
378
    }
379
380
    /**
381
     * @param SapiInterface         $sapi
382
     * @param PsrContainerInterface $container
383
     * @param callable              $requestFactory
384
     *
385
     * @return ServerRequestInterface
386
     */
387 7
    private function createRequest(
388
        SapiInterface $sapi,
389
        PsrContainerInterface $container,
390
        callable $requestFactory
391
    ): ServerRequestInterface {
392
        // check the factory method signature
393 7
        assert(
394 7
            $this->checkPublicStaticCallable(
395 7
                $requestFactory,
396 7
                [SapiInterface::class, PsrContainerInterface::class],
397 7
                ServerRequestInterface::class
398
            ),
399
            'Factory method should have signature ' .
400 7
            '`public static methodName(SapiInterface, PsrContainerInterface): ServerRequestInterface`'
401
        );
402
403 7
        $request = call_user_func($requestFactory, $sapi, $container);
404
405 7
        return $request;
406
    }
407
408
    /**
409
     * @param array $allowedMethods
410
     *
411
     * @return Closure
412
     */
413
    private function createMethodNotAllowedTerminalHandler(array $allowedMethods): Closure
414
    {
415
        // 405 Method Not Allowed
416 1
        return function () use ($allowedMethods): ResponseInterface {
417 1
            return $this->createEmptyResponse(405, ['Accept' => implode(',', $allowedMethods)]);
418 1
        };
419
    }
420
421
    /**
422
     * @return Closure
423
     */
424
    private function createNotFoundTerminalHandler(): Closure
425
    {
426
        // 404 Not Found
427 1
        return function (): ResponseInterface {
428 1
            return $this->createEmptyResponse(404);
429 1
        };
430
    }
431
432
    /**
433
     * @param Closure               $handler
434
     * @param PsrContainerInterface $container
435
     * @param array|null            $middleware
436
     *
437
     * @return Closure
438
     */
439 6
    private function createMiddlewareChainImpl(
440
        Closure $handler,
441
        PsrContainerInterface $container,
442
        array $middleware = null
443
    ): Closure {
444 6
        if (empty($middleware) === false) {
445 6
            $start = count($middleware) - 1;
446 6
            for ($index = $start; $index >= 0; $index--) {
447 6
                $handler = $this->createMiddlewareChainLink($handler, $middleware[$index], $container);
448
            }
449
        }
450
451 6
        return $handler;
452
    }
453
454
    /**
455
     * @param Closure               $next
456
     * @param callable              $middleware
457
     * @param PsrContainerInterface $container
458
     *
459
     * @return Closure
460
     */
461 6
    private function createMiddlewareChainLink(
462
        Closure $next,
463
        callable $middleware,
464
        PsrContainerInterface $container
465
    ): Closure {
466
        // check the middleware method signature
467 6
        assert(
468 6
            $this->checkPublicStaticCallable(
469 6
                $middleware,
470 6
                [ServerRequestInterface::class, Closure::class, PsrContainerInterface::class],
471 6
                ResponseInterface::class
472
            ),
473
            'Middleware method should have signature ' .
474 6
            '`public static methodName(ServerRequestInterface, Closure, PsrContainerInterface): ResponseInterface`'
475
        );
476
477 6
        return function (ServerRequestInterface $request) use ($next, $middleware, $container): ResponseInterface {
478 6
            return call_user_func($middleware, $request, $next, $container);
479 6
        };
480
    }
481
}
482