Completed
Push — master ( 9f405c...1787fc )
by Neomerx
05:56
created

Application   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 438
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 19
dl 0
loc 438
ccs 137
cts 137
cp 1
rs 10
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
createSettingsProvider() 0 1 ?
createContainerInstance() 0 1 ?
A callHandler() 0 21 1
A createMethodNotAllowedTerminalHandler() 0 7 1
A hp$0 ➔ __construct() 0 5 1
A setSapi() 0 6 1
C run() 0 58 10
A getDefaultRequestFactory() 0 4 1
A defaultRequestFactory() 0 15 1
A handleRequest() 0 8 1
A handleThrowable() 0 14 3
A createEmptyResponse() 0 6 1
A createDefaultThrowableResponse() 0 20 1
A getRouter() 0 4 1
B configureContainer() 0 18 5
A addMiddlewareChain() 0 11 1
A initRouter() 0 12 1
A createRequest() 0 20 1
A createTerminalHandler() 0 17 2
A createNotFoundTerminalHandler() 0 7 1
A createMiddlewareChainImpl() 0 12 2
A createMiddlewareChainLink() 0 20 1
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\Contracts\Settings\SettingsProviderInterface;
27
use Limoncello\Core\Contracts\CoreSettingsInterface;
28
use Limoncello\Core\Reflection\CheckCallableTrait;
29
use Limoncello\Core\Routing\Router;
30
use LogicException;
31
use Psr\Container\ContainerInterface as PsrContainerInterface;
32
use Psr\Http\Message\RequestInterface;
33
use Psr\Http\Message\ResponseInterface;
34
use Psr\Http\Message\ServerRequestInterface;
35
use Throwable;
36
use Zend\Diactoros\Response\EmptyResponse;
37
use Zend\Diactoros\Response\TextResponse;
38
use Zend\Diactoros\ServerRequest;
39
40
/**
41
 * @package Limoncello\Core
42
 *
43
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
44
 */
45
abstract class Application implements ApplicationInterface
46
{
47
    use CheckCallableTrait;
48
49
    /** Method name for default request factory. */
50
    const FACTORY_METHOD = 'defaultRequestFactory';
51
52
    /** HTTP error code for default error response. */
53
    protected const DEFAULT_HTTP_ERROR_CODE = 500;
54
55
    /**
56
     * @var SapiInterface|null
57
     */
58
    private $sapi;
59
60
    /**
61
     * @var RouterInterface|null
62
     */
63
    private $router = null;
64
65
    /**
66
     * @return SettingsProviderInterface
67
     */
68
    abstract protected function createSettingsProvider(): SettingsProviderInterface;
69
70
    /**
71
     * @return LimoncelloContainerInterface
72
     */
73
    abstract protected function createContainerInstance(): LimoncelloContainerInterface;
74
75
    /**
76
     * @inheritdoc
77
     */
78 8
    public function setSapi(SapiInterface $sapi): ApplicationInterface
79
    {
80 8
        $this->sapi = $sapi;
81
82 8
        return $this;
83
    }
84
85
    /**
86
     * @inheritdoc
87
     *
88
     * @SuppressWarnings(PHPMD.StaticAccess)
89
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
90
     */
91 9
    public function run(): void
92
    {
93 9
        if ($this->sapi === null) {
94 1
            throw new LogicException('SAPI not set.');
95
        }
96
97 8
        $container = null;
98
99
        try {
100 8
            $container = $this->createContainerInstance();
101
102 8
            $settingsProvider = $this->createSettingsProvider();
103 8
            $container->offsetSet(SettingsProviderInterface::class, $settingsProvider);
104
105 8
            $coreSettings = $settingsProvider->get(CoreSettingsInterface::class);
106
107
            // match route from `Request` to handler, route container configurators/middleware, etc
108
            list($matchCode, $allowedMethods, $handlerParams, $handler,
109 8
                $routeMiddleware, $routeConfigurators, $requestFactory) = $this->initRouter($coreSettings)
110 8
                ->match($this->sapi->getMethod(), $this->sapi->getUri()->getPath());
111
112
            // configure container
113 8
            $globalConfigurators = BaseCoreSettings::getGlobalConfiguratorsFromData($coreSettings);
114 8
            $this->configureContainer($container, $globalConfigurators, $routeConfigurators);
115
116
            // build pipeline for handling `Request`: global middleware -> route middleware -> handler (e.g. controller)
117
118
            // select terminal handler
119
            switch ($matchCode) {
120 8
                case RouterInterface::MATCH_FOUND:
121 6
                    $handler = $this->createTerminalHandler($handler, $handlerParams, $container);
122 6
                    break;
123 2
                case RouterInterface::MATCH_METHOD_NOT_ALLOWED:
124 1
                    $handler = $this->createMethodNotAllowedTerminalHandler($allowedMethods);
125 1
                    break;
126
                default:
127 1
                    assert($matchCode === RouterInterface::MATCH_NOT_FOUND);
128 1
                    $handler = $this->createNotFoundTerminalHandler();
129 1
                    break;
130
            }
131
132 8
            $globalMiddleware = BaseCoreSettings::getGlobalMiddlewareFromData($coreSettings);
133 8
            $hasMiddleware    = empty($globalMiddleware) === false || empty($routeMiddleware) === false;
134
135 8
            $handler = $hasMiddleware === true ?
136 8
                $this->addMiddlewareChain($handler, $container, $globalMiddleware, $routeMiddleware) : $handler;
137
138 8
            $request = $requestFactory === null && $hasMiddleware === false && $matchCode === RouterInterface::MATCH_FOUND ?
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 124 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

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