Test Failed
Branch master (effa58)
by Divine Niiquaye
02:13
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.4 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
    protected ResponseFactoryInterface $responseFactory;
45
46 39
    /** @var callable */
47 39
    protected $handlerResolver;
48 39
49
    public function __construct(ResponseFactoryInterface $responseFactory, callable $handlerResolver = null)
50
    {
51
        $this->responseFactory = $responseFactory;
52
        $this->handlerResolver = $handlerResolver ?? new RouteInvoker();
53
    }
54
55 37
    /**
56
     * {@inheritdoc}
57 37
     *
58
     * @throws RouteNotFoundException|InvalidControllerException
59 37
     */
60
    public function handle(ServerRequestInterface $request): ResponseInterface
61 37
    {
62 37
        $route = $request->getAttribute(Route::class);
63
64
        if (!$route instanceof Route) {
65 37
            if (true === $notFoundResponse = $request->getAttribute(static::OVERRIDE_HTTP_RESPONSE)) {
66 3
                return $this->responseFactory->createResponse();
67 3
            }
68
69 3
            if ($notFoundResponse instanceof ResponseInterface) {
70
                return $notFoundResponse;
71
            }
72 34
73
            throw new RouteNotFoundException(\sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getUri()->getPath()), 404);
74
        }
75
76
        // Resolve route handler arguments ...
77
        if (!$response = $this->resolveRoute($route, $request)) {
78
            throw new InvalidControllerException('The route handler\'s content is not a valid PSR7 response body stream.');
79
        }
80
81
        if (!$response instanceof ResponseInterface) {
82
            ($result = $this->responseFactory->createResponse())->getBody()->write($response);
83
            $response = $result;
84
        }
85 34
86
        return $response->hasHeader(self::CONTENT_TYPE) ? $response : $this->negotiateContentType($response);
87
    }
88 34
89 24
    /**
90
     * @return ResponseInterface|string|false
91
     */
92 10
    protected function resolveRoute(Route $route, ServerRequestInterface $request)
93 1
    {
94
        \ob_start(); // Start buffering response output
95
96
        try {
97 10
            // The route handler to resolve ...
98
            $response = $this->resolveHandler($request, $route);
99
100
            if ($response instanceof ResponseInterface || \is_string($response)) {
101
                return $response;
102
            }
103
104
            if (\is_array($response) || \is_iterable($response) || $response instanceof \JsonSerializable) {
105 10
                $response = \json_encode($response, \PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0);
106
            }
107 10
        } catch (\Throwable $e) {
108
            \ob_get_clean();
109 10
110 1
            throw $e;
111
        } finally {
112
            while (\ob_get_level() > 1) {
113 9
                $response = \ob_get_clean(); // If more than one output buffers is set ...
114 1
            }
115
        }
116
117
        return $response ?? \ob_get_clean();
118 8
    }
119 7
120
    /**
121
     * A HTTP response Content-Type header negotiator for html, json, svg, xml, and plain-text.
122 1
     */
123
    protected function negotiateContentType(ResponseInterface $response): ResponseInterface
124
    {
125
        $contents = (string) $response->getBody();
126
        $contentType = 'text/html; charset=utf-8'; // Default content type.
127
128
        if (1 === $matched = \preg_match(static::CONTENT_REGEX, $contents, $matches, \PREG_UNMATCHED_AS_NULL)) {
129
            if (null === $matches[2]) {
130 10
                $contentType = 'application/json';
131
            } elseif ('xml' === $matches[1]) {
132 10
                $contentType = 'svg' === $matches[2] ? 'image/svg+xml' : \sprintf('application/%s; charset=utf-8', 'rss' === $matches[2] ? 'rss+xml' : 'xml');
133
            }
134 10
        } elseif (0 === $matched) {
135
            $contentType = 'text/plain; charset=utf-8';
136
        }
137
138
        return $response->withHeader(self::CONTENT_TYPE, $contentType);
139
    }
140
141
    /**
142 9
     * @internal
143
     *
144 9
     * @return mixed
145 9
     */
146 9
    private function resolveHandler(ServerRequestInterface $request, Route $route)
147
    {
148 9
        $handler = $route->get('handler');
149
150
        if ($handler instanceof RequestHandlerInterface) {
151
            return $handler->handle($request);
152
        }
153
154
        if (!$handler instanceof ResponseInterface) {
155
            $parameters = $route->get('arguments');
156
157
            foreach ([$request, $this->responseFactory] as $psr7) {
158
                $parameters[\get_class($psr7)] = $psr7;
159
160
                foreach ((@\class_implements($psr7) ?: []) as $psr7Interface) {
161
                    $parameters[$psr7Interface] = $psr7;
162
                }
163
            }
164
165
            if ($handler instanceof ResourceHandler) {
166
                $handler = $handler($request->getMethod());
167
            }
168
169
            $handler = ($this->handlerResolver)($handler, $parameters);
170
171
            if ($handler instanceof RequestHandlerInterface) {
172
                return $handler->handle($request);
173
            }
174
        }
175
176
        return true === $handler ? null : $handler;
177
    }
178
}
179