Completed
Push — master ( 163fd9...314ef3 )
by Neomerx
02:36
created

Application::run()   D

Complexity

Conditions 9
Paths 49

Size

Total Lines 43
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 9

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 43
ccs 28
cts 28
cp 1
rs 4.909
cc 9
eloc 27
nc 49
nop 0
crap 9
1
<?php namespace Limoncello\Core\Application;
2
3
/**
4
 * Copyright 2015-2016 [email protected] (www.neomerx.com)
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 FastRoute\DataGenerator\GroupCountBased as GroupCountBasedGenerator;
21
use Interop\Container\ContainerInterface;
22
use Limoncello\Core\Contracts\Application\ApplicationInterface;
23
use Limoncello\Core\Contracts\Application\SapiInterface;
24
use Limoncello\Core\Contracts\Config\ConfigInterface;
25
use Limoncello\Core\Contracts\Routing\RouterInterface;
26
use Limoncello\Core\Routing\Dispatcher\GroupCountBased as GroupCountBasedDispatcher;
27
use Limoncello\Core\Routing\Router;
28
use LogicException;
29
use Psr\Http\Message\ResponseInterface;
30
use Psr\Http\Message\ServerRequestInterface;
31
use Zend\Diactoros\Response;
32
use Zend\Diactoros\Response\EmptyResponse;
33
use Zend\Diactoros\ServerRequest;
34
35
/**
36
 * @package Limoncello\Core
37
 *
38
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
39
 */
40
abstract class Application implements ApplicationInterface
41
{
42
    /** Method name for default request factory. */
43
    const FACTORY_METHOD = 'defaultRequestFactory';
44
45
    /**
46
     * @var SapiInterface|null
47
     */
48
    private $sapi;
49
50
    /**
51
     * @var RouterInterface
52
     */
53
    private $router;
54
55
    /**
56
     * @return ContainerInterface
57
     */
58
    abstract protected function createContainer();
59
60
    /**
61
     * @return array
62
     */
63
    abstract protected function getRoutesData();
64
65
    /**
66
     * @return callable[]
67
     */
68
    abstract protected function getGlobalMiddleware();
69
70
    /**
71
     * @param SapiInterface      $sapi
72
     * @param ContainerInterface $container
73
     *
74
     * @return void
75
     */
76
    abstract protected function setUpExceptionHandler(SapiInterface $sapi, ContainerInterface $container);
77
78
    /**
79
     * @inheritdoc
80
     */
81 5
    public function setSapi(SapiInterface $sapi)
82
    {
83 5
        $this->sapi = $sapi;
84
85 5
        return $this;
86
    }
87
88
    /**
89
     * @inheritdoc
90
     */
91 6
    public function run()
92
    {
93 6
        if ($this->sapi === null) {
94 1
            throw new LogicException('SAPI not set.');
95
        }
96
97 5
        $userContainer = $this->createContainer();
98
99 5
        $this->setUpExceptionHandler($this->sapi, $userContainer);
100
101
        list($matchCode, $allowedMethods, $handlerParams, $handler, $routeMiddleware, $configurators, $requestFactory) =
102 5
            $this->getRouter()->match($this->sapi->getMethod(), $this->sapi->getUri()->getPath());
103
104 5
        if (empty($configurators) === false) {
105 2
            $this->configureUserContainer($userContainer, $configurators);
106 2
        }
107
108
        switch ($matchCode) {
109 5
            case RouterInterface::MATCH_FOUND:
110 3
                $handler = $this->createOrdinaryTerminalHandler($handler, $handlerParams, $userContainer);
111 3
                break;
112 2
            case RouterInterface::MATCH_METHOD_NOT_ALLOWED:
113 1
                $handler = $this->createMethodNotAllowedTerminalHandler($allowedMethods);
114 1
                break;
115 1
            default:
116 1
                $handler = $this->createNotFoundTerminalHandler();
117 1
                break;
118 1
        }
119
120 5
        $globalMiddleware = $this->getGlobalMiddleware();
121 5
        $handler = $this->createMiddlewareChain($handler, $userContainer, $globalMiddleware, $routeMiddleware);
122
123
        // if handler consists only from Controller and user didn't asked for Request creation
124 5
        $requestNotNeeded = $requestFactory === null && empty($globalMiddleware) === true && empty($routeMiddleware) &&
125 5
            $matchCode === RouterInterface::MATCH_FOUND;
126 5
        $request = $requestNotNeeded === true ?
127 5
            null : $this->createRequest($this->sapi, $userContainer, $requestFactory);
128
129
        // send Request down all middleware (global then route's) and call terminal handler in Controller
130 5
        $response = $handler($request);
131
132 5
        $this->sapi->handleResponse($response);
133 5
    }
134
135
    /**
136
     * @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...
137
     */
138 5
    public static function getDefaultRequestFactory()
139
    {
140 5
        return [static::class, static::FACTORY_METHOD];
141
    }
142
143
    /**
144
     * @param SapiInterface $sapi
145
     *
146
     * @return ServerRequestInterface
147
     */
148 3
    public static function defaultRequestFactory(SapiInterface $sapi)
149
    {
150 3
        return new ServerRequest(
151 3
            $sapi->getServer(),
152 3
            $sapi->getFiles(),
153 3
            $sapi->getUri(),
154 3
            $sapi->getMethod(),
155 3
            $sapi->getRequestBody(),
156 3
            $sapi->getHeaders(),
157 3
            $sapi->getCookies(),
158 3
            $sapi->getQueryParams(),
159 3
            $sapi->getParsedBody()
160 3
        );
161
    }
162
163
    /**
164
     * @param int   $status
165
     * @param array $headers
166
     *
167
     * @return ResponseInterface
168
     */
169 2
    protected function createEmptyResponse($status = 204, array $headers = [])
170
    {
171 2
        $response = new EmptyResponse($status, $headers);
172
173 2
        return $response;
174
    }
175
176
    /**
177
     * @return RouterInterface
178
     */
179 5
    protected function getRouter()
180
    {
181 5
        if ($this->router === null) {
182 5
            $this->router = $this->createRouter();
183 5
            $this->router->loadCachedRoutes($this->getRoutesData());
184 5
        }
185
186 5
        return $this->router;
187
    }
188
189
    /**
190
     * @return array
191
     */
192 5
    protected function getCoreConfig()
193
    {
194
        return [
195 5
            ConfigInterface::KEY_ROUTING_GENERATOR  => GroupCountBasedGenerator::class,
196 5
            ConfigInterface::KEY_ROUTING_DISPATCHER => GroupCountBasedDispatcher::class,
197 5
        ];
198
    }
199
200
    /**
201
     * @return RouterInterface
202
     */
203 5
    private function createRouter()
204
    {
205 5
        $config = $this->getCoreConfig();
206
207 5
        $generatorClass = $this->getValue(
208 5
            $config,
209 5
            ConfigInterface::KEY_ROUTING_GENERATOR,
210
            GroupCountBasedGenerator::class
211 5
        );
212 5
        $dispatcherClass = $this->getValue(
213 5
            $config,
214 5
            ConfigInterface::KEY_ROUTING_DISPATCHER,
215
            GroupCountBasedDispatcher::class
216 5
        );
217
218 5
        $router = new Router($generatorClass, $dispatcherClass);
219
220 5
        return $router;
221
    }
222
223
    /**
224
     * @param ContainerInterface $container
225
     * @param callable[]         $configurators
226
     *
227
     * @return void
228
     */
229 2
    private function configureUserContainer(ContainerInterface $container, array $configurators)
230
    {
231 2
        foreach ($configurators as $configurator) {
232 2
            call_user_func($configurator, $container);
233 2
        }
234 2
    }
235
236
    /**
237
     * @param SapiInterface      $sapi
238
     * @param ContainerInterface $userContainer
239
     * @param callable|null      $requestFactory
240
     *
241
     * @return ServerRequestInterface
242
     */
243 4
    private function createRequest(
244
        SapiInterface $sapi,
245
        ContainerInterface $userContainer,
246
        callable $requestFactory = null
247
    ) {
248 4
        $request = call_user_func(
249 4
            $requestFactory === null ? self::getDefaultRequestFactory() : $requestFactory,
250 4
            $sapi,
251
            $userContainer
252 4
        );
253
254 4
        return $request;
255
    }
256
257
    /**
258
     * @param callable           $handler
259
     * @param array              $handlerParams
260
     * @param ContainerInterface $container
261
     *
262
     * @return Closure
263
     */
264 3
    private function createOrdinaryTerminalHandler(
265
        callable $handler,
266
        array $handlerParams,
267
        ContainerInterface $container
268
    ) {
269
        return function (ServerRequestInterface $request = null) use ($handler, $handlerParams, $container) {
270 3
            return call_user_func($handler, $handlerParams, $container, $request);
271 3
        };
272
    }
273
274
    /**
275
     * @param array $allowedMethods
276
     *
277
     * @return Closure
278
     */
279 1
    private function createMethodNotAllowedTerminalHandler(array $allowedMethods)
280
    {
281
        // 405 Method Not Allowed
282
        return function () use ($allowedMethods) {
283 1
            return $this->createEmptyResponse(405, ['Accept' => implode(',', $allowedMethods)]);
284 1
        };
285
    }
286
287
    /**
288
     * @return Closure
289
     */
290 1
    private function createNotFoundTerminalHandler()
291
    {
292
        // 404 Not Found
293
        return function () {
294 1
            return $this->createEmptyResponse(404);
295 1
        };
296
    }
297
298
    /**
299
     * @param Closure            $handler
300
     * @param ContainerInterface $userContainer
301
     * @param array|null         $globalMiddleware
302
     * @param array|null         $routeMiddleware
303
     *
304
     * @return Closure
305
     */
306 5
    private function createMiddlewareChain(
307
        Closure $handler,
308
        ContainerInterface $userContainer,
309
        array $globalMiddleware,
310
        array $routeMiddleware = null
311
    ) {
312 5
        $handler = $this->createMiddlewareChainImpl($handler, $userContainer, $routeMiddleware);
313 5
        $handler = $this->createMiddlewareChainImpl($handler, $userContainer, $globalMiddleware);
314
315 5
        return $handler;
316
    }
317
318
    /**
319
     * @param Closure            $handler
320
     * @param ContainerInterface $userContainer
321
     * @param array|null         $middleware
322
     *
323
     * @return Closure
324
     */
325 5
    private function createMiddlewareChainImpl(
326
        Closure $handler,
327
        ContainerInterface $userContainer,
328
        array $middleware = null
329
    ) {
330 5
        for ($index = count($middleware) - 1; $index >= 0; $index--) {
331 4
            $handler = $this->createMiddlewareChainLink($handler, $middleware[$index], $userContainer);
332 4
        }
333
334 5
        return $handler;
335
    }
336
337
    /**
338
     * @param Closure            $next
339
     * @param callable           $middleware
340
     * @param ContainerInterface $userContainer
341
     *
342
     * @return Closure
343
     */
344
    private function createMiddlewareChainLink(Closure $next, callable $middleware, ContainerInterface $userContainer)
345
    {
346 4
        return function (ServerRequestInterface $request) use ($next, $middleware, $userContainer) {
347 4
            return call_user_func($middleware, $request, $next, $userContainer);
348 4
        };
349
    }
350
351
    /**
352
     * @param array      $config
353
     * @param int|string $key
354
     * @param mixed      $default
355
     *
356
     * @return mixed
357
     */
358 5
    private function getValue(array $config, $key, $default)
359
    {
360 5
        $value = array_key_exists($key, $config) === true ? $config[$key] : $default;
361
362 5
        return $value;
363
    }
364
}
365