Test Failed
Pull Request — master (#16)
by Divine Niiquaye
02:34
created

RouteHandler::resolveRoute()   B

Complexity

Conditions 9
Paths 60

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 9

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 9
eloc 14
nc 60
nop 2
dl 0
loc 26
ccs 9
cts 9
cp 1
crap 9
rs 8.0555
c 2
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Flight Routing.
7
 *
8
 * PHP version 7.1 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Flight\Routing\Handlers;
19
20
use Flight\Routing\Routes\FastRoute as Route;
21
use Flight\Routing\Exceptions\{InvalidControllerException, RouteNotFoundException};
22
use Psr\Http\Message\{ResponseFactoryInterface, ResponseInterface, ServerRequestInterface};
23
use Psr\Http\Server\RequestHandlerInterface;
24
25
/**
26
 * Default routing request handler.
27
 *
28
 * if route is found in request attribute, dispatch the route handler's
29
 * response to the browser and provides ability to detect right response content-type.
30
 *
31
 * @author Divine Niiquaye Ibok <[email protected]>
32
 */
33
class RouteHandler implements RequestHandlerInterface
34
{
35
    /**
36
     * This allows a response to be served when no route is found.
37
     */
38
    public const OVERRIDE_HTTP_RESPONSE = ResponseInterface::class;
39
40
    protected const CONTENT_TYPE = 'Content-Type';
41
42
    protected const CONTENT_REGEX = '#(?|\{\"[\w\,\"\:\[\]]+\}|\<(?|\?(xml)|\w+).*>.*<\/(\w+)>)$#s';
43
44 39
    /** @var ResponseFactoryInterface */
45
    protected $responseFactory;
46 39
47 39
    /** @var callable */
48 39
    protected $handlerResolver;
49
50
    public function __construct(ResponseFactoryInterface $responseFactory, callable $handlerResolver = null)
51
    {
52
        $this->responseFactory = $responseFactory;
53
        $this->handlerResolver = $handlerResolver ?? new RouteInvoker();
54
    }
55 37
56
    /**
57 37
     * {@inheritdoc}
58
     *
59 37
     * @throws RouteNotFoundException|InvalidControllerException
60
     */
61 37
    public function handle(ServerRequestInterface $request): ResponseInterface
62 37
    {
63
        $route = $request->getAttribute(Route::class);
64
65 37
        if (!$route instanceof Route) {
66 3
            if (true === $notFoundResponse = $request->getAttribute(static::OVERRIDE_HTTP_RESPONSE)) {
67 3
                return $this->responseFactory->createResponse();
68
            }
69 3
70
            if ($notFoundResponse instanceof ResponseInterface) {
71
                return $notFoundResponse;
72 34
            }
73
74
            throw new RouteNotFoundException(\sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getUri()->getPath()), 404);
75
        }
76
77
        // Resolve route handler arguments ...
78
        if (!$response = $this->resolveRoute($route, $request)) {
79
            throw new InvalidControllerException('The route handler\'s content is not a valid PSR7 response body stream.');
80
        }
81
82
        if (!$response instanceof ResponseInterface) {
83
            ($result = $this->responseFactory->createResponse())->getBody()->write($response);
84
            $response = $result;
85 34
        }
86
87
        return $response->hasHeader(self::CONTENT_TYPE) ? $response : $this->negotiateContentType($response);
88 34
    }
89 24
90
    /**
91
     * @return ResponseInterface|string|false
92 10
     */
93 1
    protected function resolveRoute(Route $route, ServerRequestInterface $request)
94
    {
95
        \ob_start(); // Start buffering response output
96
97 10
        try {
98
            // The route handler to resolve ...
99
            $response = $this->resolveHandler($request, $route);
100
101
            if ($response instanceof ResponseInterface || \is_string($response)) {
102
                return $response;
103
            }
104
105 10
            if (\is_array($response) || \is_iterable($response) || $response instanceof \JsonSerializable) {
106
                $response = \json_encode($response, \PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0);
107 10
            }
108
        } catch (\Throwable $e) {
109 10
            \ob_get_clean();
110 1
111
            throw $e;
112
        } finally {
113 9
            while (\ob_get_level() > 1) {
114 1
                $response = \ob_get_clean(); // If more than one output buffers is set ...
115
            }
116
        }
117
118 8
        return $response ?? ob_get_clean();
119 7
    }
120
121
    /**
122 1
     * A HTTP response Content-Type header negotiator for html, json, svg, xml, and plain-text.
123
     */
124
    protected function negotiateContentType(ResponseInterface $response): ResponseInterface
125
    {
126
        $contents = (string) $response->getBody();
127
        $contentType = 'text/html; charset=utf-8'; // Default content type.
128
129
        if (1 === $matched = \preg_match(static::CONTENT_REGEX, $contents, $matches, \PREG_UNMATCHED_AS_NULL)) {
130 10
            if (null === $matches[2]) {
131
                $contentType = 'application/json';
132 10
            } elseif ('xml' === $matches[1]) {
133
                $contentType = 'svg' === $matches[2] ? 'image/svg+xml' : \sprintf('application/%s; charset=utf-8', 'rss' === $matches[2] ? 'rss+xml' : 'xml');
134 10
            }
135
        } elseif (0 === $matched) {
136
            $contentType = 'text/plain; charset=utf-8';
137
        }
138
139
        return $response->withHeader(self::CONTENT_TYPE, $contentType);
140
    }
141
142 9
    /**
143
     * @internal
144 9
     *
145 9
     * @return mixed
146 9
     */
147
    private function resolveHandler(ServerRequestInterface $request, Route $route)
148 9
    {
149
        $handler = $route->get('handler');
150
151
        if ($handler instanceof RequestHandlerInterface) {
152
            return $handler->handle($request);
153
        }
154
155
        if (!$handler instanceof ResponseInterface) {
156
            $parameters = $route->get('arguments');
157
158
            foreach ([$request, $this->responseFactory] as $psr7) {
159
                $parameters[\get_class($psr7)] = $psr7;
160
161
                foreach ((@\class_implements($psr7) ?: []) as $psr7Interface) {
162
                    $parameters[$psr7Interface] = $psr7;
163
                }
164
            }
165
166
            if ($handler instanceof ResourceHandler) {
167
                $handler = $handler($request->getMethod());
168
            }
169
170
            $handler = ($this->handlerResolver)($handler, $parameters);
171
172
            if ($handler instanceof RequestHandlerInterface) {
173
                return $handler->handle($request);
174
            }
175
        }
176
177
        return true === $handler ? null : $handler;
178
    }
179
}
180