Completed
Branch master (8ae5a6)
by Neomerx
01:35
created

Application::setUpExceptionHandler()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1
ccs 0
cts 0
cp 0
c 0
b 0
f 0
nc 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\Routing\RouterInterface;
24
use Limoncello\Contracts\Settings\SettingsProviderInterface;
25
use Limoncello\Core\Contracts\CoreSettingsInterface;
26
use Limoncello\Core\Reflection\CheckCallableTrait;
27
use Limoncello\Core\Routing\Router;
28
use LogicException;
29
use Psr\Container\ContainerInterface as PsrContainerInterface;
30
use Psr\Http\Message\RequestInterface;
31
use Psr\Http\Message\ResponseInterface;
32
use Psr\Http\Message\ServerRequestInterface;
33
use Zend\Diactoros\Response\EmptyResponse;
34
use Zend\Diactoros\ServerRequest;
35
36
/**
37
 * @package Limoncello\Core
38
 *
39
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
40
 */
41
abstract class Application implements ApplicationInterface
42
{
43
    use CheckCallableTrait;
44
45
    /** Method name for default request factory. */
46
    const FACTORY_METHOD = 'defaultRequestFactory';
47
48
    /**
49
     * @var SapiInterface|null
50
     */
51
    private $sapi;
52
53
    /**
54
     * @var RouterInterface|null
55
     */
56
    private $router = null;
57
58
    /**
59
     * @return SettingsProviderInterface
60
     */
61
    abstract protected function createSettingsProvider(): SettingsProviderInterface;
62
63
    /**
64
     * @return LimoncelloContainerInterface
65
     */
66
    abstract protected function createContainerInstance(): LimoncelloContainerInterface;
67
68
    /**
69
     * Exception handler should not use container before actual error occurs.
70
     *
71
     * @param SapiInterface         $sapi
72
     * @param PsrContainerInterface $container
73
     *
74
     * @return void
75
     */
76
    abstract protected function setUpExceptionHandler(SapiInterface $sapi, PsrContainerInterface $container): void;
77
78
    /**
79
     * @inheritdoc
80
     */
81 6
    public function setSapi(SapiInterface $sapi): ApplicationInterface
82
    {
83 6
        $this->sapi = $sapi;
84
85 6
        return $this;
86
    }
87
88
    /**
89
     * @inheritdoc
90
     *
91
     * @SuppressWarnings(PHPMD.StaticAccess)
92
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
93
     */
94 7
    public function run(): void
95
    {
96 7
        if ($this->sapi === null) {
97 1
            throw new LogicException('SAPI not set.');
98
        }
99
100 6
        $container = $this->createContainerInstance();
101
102 6
        $this->setUpExceptionHandler($this->sapi, $container);
103
104 6
        $settingsProvider = $this->createSettingsProvider();
105 6
        $container->offsetSet(SettingsProviderInterface::class, $settingsProvider);
106
107 6
        $coreSettings = $settingsProvider->get(CoreSettingsInterface::class);
108
109
        // match route from `Request` to handler, route container configurators/middleware, etc
110
        list($matchCode, $allowedMethods, $handlerParams, $handler,
111 6
            $routeMiddleware, $routeConfigurators, $requestFactory) = $this->initRouter($coreSettings)
112 6
                ->match($this->sapi->getMethod(), $this->sapi->getUri()->getPath());
113
114
        // configure container
115 6
        $globalConfigurators = BaseCoreSettings::getGlobalConfiguratorsFromData($coreSettings);
116 6
        $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 6
            case RouterInterface::MATCH_FOUND:
123 4
                $handler = $this->createTerminalHandler($handler, $handlerParams, $container);
124 4
                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 6
        $globalMiddleware = BaseCoreSettings::getGlobalMiddlewareFromData($coreSettings);
135 6
        $hasMiddleware    = empty($globalMiddleware) === false || empty($routeMiddleware) === false;
136
137 6
        $handler = $hasMiddleware === true ?
138 6
            $this->addMiddlewareChain($handler, $container, $globalMiddleware, $routeMiddleware) : $handler;
139
140 6
        $request = $requestFactory === null && $hasMiddleware === false && $matchCode === RouterInterface::MATCH_FOUND ?
141 1
            null :
142 6
            $this->createRequest($this->sapi, $container, $requestFactory ?? static::getDefaultRequestFactory());
143
144
        // Execute the pipeline by sending `Request` down all middleware (global then route's then
145
        // terminal handler in `Controller` and back) and then send `Response` to SAPI
146 6
        $this->sapi->handleResponse($this->handleRequest($handler, $request));
147
    }
148
149
    /**
150
     * @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...
151
     */
152 6
    public static function getDefaultRequestFactory(): callable
153
    {
154 6
        return [static::class, static::FACTORY_METHOD];
155
    }
156
157
    /**
158
     * @param SapiInterface $sapi
159
     *
160
     * @return ServerRequestInterface
161
     */
162 4
    public static function defaultRequestFactory(SapiInterface $sapi): ServerRequestInterface
163
    {
164 4
        return new ServerRequest(
165 4
            $sapi->getServer(),
166 4
            $sapi->getFiles(),
167 4
            $sapi->getUri(),
168 4
            $sapi->getMethod(),
169 4
            $sapi->getRequestBody(),
170 4
            $sapi->getHeaders(),
171 4
            $sapi->getCookies(),
172 4
            $sapi->getQueryParams(),
173 4
            $sapi->getParsedBody(),
174 4
            $sapi->getProtocolVersion()
175
        );
176
    }
177
178
    /**
179
     * @param Closure               $handler
180
     * @param RequestInterface|null $request
181
     *
182
     * @return ResponseInterface
183
     */
184 6
    protected function handleRequest(Closure $handler, RequestInterface $request = null): ResponseInterface
185
    {
186 6
        $response = call_user_func($handler, $request);
187
188 6
        assert($response instanceof ResponseInterface);
189
190 6
        return $response;
191
    }
192
193
    /**
194
     * @param int   $status
195
     * @param array $headers
196
     *
197
     * @return ResponseInterface
198
     */
199 2
    protected function createEmptyResponse($status = 204, array $headers = []): ResponseInterface
200
    {
201 2
        $response = new EmptyResponse($status, $headers);
202
203 2
        return $response;
204
    }
205
206
    /**
207
     * @return RouterInterface|null
208
     */
209 1
    protected function getRouter(): ?RouterInterface
210
    {
211 1
        return $this->router;
212
    }
213
214
    /**
215
     * @param LimoncelloContainerInterface $container
216
     * @param callable[]|null              $globalConfigurators
217
     * @param callable[]|null              $routeConfigurators
218
     *
219
     * @return void
220
     */
221 6
    protected function configureContainer(
222
        LimoncelloContainerInterface $container,
223
        array $globalConfigurators = null,
224
        array $routeConfigurators = null
225
    ): void {
226 6
        if (empty($globalConfigurators) === false) {
227 6
            foreach ($globalConfigurators as $configurator) {
228 6
                assert($this->checkPublicStaticCallable($configurator, [LimoncelloContainerInterface::class]));
229 6
                $configurator($container);
230
            }
231
        }
232 6
        if (empty($routeConfigurators) === false) {
233 2
            foreach ($routeConfigurators as $configurator) {
234 2
                assert($this->checkPublicStaticCallable($configurator, [LimoncelloContainerInterface::class]));
235 2
                $configurator($container);
236
            }
237
        }
238
    }
239
240
    /**
241
     * @param Closure               $handler
242
     * @param PsrContainerInterface $container
243
     * @param array|null            $globalMiddleware
244
     * @param array|null            $routeMiddleware
245
     *
246
     * @return Closure
247
     */
248 5
    protected function addMiddlewareChain(
249
        Closure $handler,
250
        PsrContainerInterface $container,
251
        array $globalMiddleware,
252
        array $routeMiddleware = null
253
    ): Closure {
254 5
        $handler = $this->createMiddlewareChainImpl($handler, $container, $routeMiddleware);
255 5
        $handler = $this->createMiddlewareChainImpl($handler, $container, $globalMiddleware);
256
257 5
        return $handler;
258
    }
259
260
    /**
261
     * @param callable                    $handler
262
     * @param array                       $handlerParams
263
     * @param PsrContainerInterface       $container
264
     * @param ServerRequestInterface|null $request
265
     *
266
     * @return ResponseInterface
267
     */
268 4
    protected function callHandler(
269
        callable $handler,
270
        array $handlerParams,
271
        PsrContainerInterface $container,
272
        ServerRequestInterface $request = null
273
    ): ResponseInterface {
274
        // check the handler method signature
275 4
        assert(
276 4
            $this->checkPublicStaticCallable(
277 4
                $handler,
278 4
                ['array', PsrContainerInterface::class, ServerRequestInterface::class],
279 4
                ResponseInterface::class
280
            ),
281
            'Handler method should have signature ' .
282 4
            '`public static methodName(array, PsrContainerInterface, ServerRequestInterface): ResponseInterface`'
283
        );
284
285 4
        $response = call_user_func($handler, $handlerParams, $container, $request);
286
287 4
        return $response;
288
    }
289
290
    /**
291
     * @param array $coreSettings
292
     *
293
     * @return RouterInterface
294
     *
295
     * @SuppressWarnings(PHPMD.StaticAccess)
296
     */
297 6
    protected function initRouter(array $coreSettings): RouterInterface
298
    {
299 6
        $routerParams    = BaseCoreSettings::getRouterParametersFromData($coreSettings);
300 6
        $routesData      = BaseCoreSettings::getRoutesDataFromData($coreSettings);
301 6
        $generatorClass  = BaseCoreSettings::getGeneratorFromParametersData($routerParams);
302 6
        $dispatcherClass = BaseCoreSettings::getDispatcherFromParametersData($routerParams);
303
304 6
        $this->router = new Router($generatorClass, $dispatcherClass);
305 6
        $this->router->loadCachedRoutes($routesData);
306
307 6
        return $this->router;
308
    }
309
310
    /**
311
     * @param SapiInterface         $sapi
312
     * @param PsrContainerInterface $container
313
     * @param callable              $requestFactory
314
     *
315
     * @return ServerRequestInterface
316
     */
317 5
    private function createRequest(
318
        SapiInterface $sapi,
319
        PsrContainerInterface $container,
320
        callable $requestFactory
321
    ): ServerRequestInterface {
322
        // check the factory method signature
323 5
        assert(
324 5
            $this->checkPublicStaticCallable(
325 5
                $requestFactory,
326 5
                [SapiInterface::class, PsrContainerInterface::class],
327 5
                ServerRequestInterface::class
328
            ),
329
            'Factory method should have signature ' .
330 5
            '`public static methodName(SapiInterface, PsrContainerInterface): ServerRequestInterface`'
331
        );
332
333 5
        $request = call_user_func($requestFactory, $sapi, $container);
334
335 5
        return $request;
336
    }
337
338
    /**
339
     * @param callable              $handler
340
     * @param array                 $handlerParams
341
     * @param PsrContainerInterface $container
342
     *
343
     * @return Closure
344
     */
345
    private function createTerminalHandler(
346
        callable $handler,
347
        array $handlerParams,
348
        PsrContainerInterface $container
349
    ): Closure {
350 4
        return function (ServerRequestInterface $request = null) use ($handler, $handlerParams, $container) {
351 4
            return $this->callHandler($handler, $handlerParams, $container, $request);
352 4
        };
353
    }
354
355
    /**
356
     * @param array $allowedMethods
357
     *
358
     * @return Closure
359
     */
360
    private function createMethodNotAllowedTerminalHandler(array $allowedMethods): Closure
361
    {
362
        // 405 Method Not Allowed
363 1
        return function () use ($allowedMethods) {
364 1
            return $this->createEmptyResponse(405, ['Accept' => implode(',', $allowedMethods)]);
365 1
        };
366
    }
367
368
    /**
369
     * @return Closure
370
     */
371
    private function createNotFoundTerminalHandler(): Closure
372
    {
373
        // 404 Not Found
374 1
        return function () {
375 1
            return $this->createEmptyResponse(404);
376 1
        };
377
    }
378
379
    /**
380
     * @param Closure               $handler
381
     * @param PsrContainerInterface $container
382
     * @param array|null            $middleware
383
     *
384
     * @return Closure
385
     */
386 5
    private function createMiddlewareChainImpl(
387
        Closure $handler,
388
        PsrContainerInterface $container,
389
        array $middleware = null
390
    ): Closure {
391 5
        $start = count($middleware) - 1;
392 5
        for ($index = $start; $index >= 0; $index--) {
393 5
            $handler = $this->createMiddlewareChainLink($handler, $middleware[$index], $container);
394
        }
395
396 5
        return $handler;
397
    }
398
399
    /**
400
     * @param Closure               $next
401
     * @param callable              $middleware
402
     * @param PsrContainerInterface $container
403
     *
404
     * @return Closure
405
     */
406 5
    private function createMiddlewareChainLink(
407
        Closure $next,
408
        callable $middleware,
409
        PsrContainerInterface $container
410
    ): Closure {
411
        // check the middleware method signature
412 5
        assert(
413 5
            $this->checkPublicStaticCallable(
414 5
                $middleware,
415 5
                [ServerRequestInterface::class, Closure::class, PsrContainerInterface::class],
416 5
                ResponseInterface::class
417
            ),
418
            'Middleware method should have signature ' .
419 5
            '`public static methodName(ServerRequestInterface, Closure, PsrContainerInterface): ResponseInterface`'
420
        );
421
422 5
        return function (ServerRequestInterface $request) use ($next, $middleware, $container): ResponseInterface {
423 5
            return call_user_func($middleware, $request, $next, $container);
424 5
        };
425
    }
426
}
427