Completed
Push — master ( 8e3397...a65237 )
by Neomerx
03:28
created

Application::handleRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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